Merge remote-tracking branch 'origin/dev' into 4.X
@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.7.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:4.8.0" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
|
||||
</settings>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-server:4.7.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-server:4.8.0" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
|
||||
</settings>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-xxl-job-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.7.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:4.8.0" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-xxl-job-admin/Dockerfile" />
|
||||
</settings>
|
||||
|
@ -10,7 +10,7 @@
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
|
||||
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
|
||||
<br>
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
@ -53,7 +53,7 @@
|
||||
| 分布式任务调度 | 采用 Xxl-Job 天生支持分布式 统一的管理中心 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
|
||||
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
|
||||
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
|
||||
| 短信 | 支持 阿里、腾讯 只需在yml配置好厂家密钥即可使用 接口化支持扩展其他厂家 | 不支持 |
|
||||
| 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 |
|
||||
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
|
||||
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
|
||||
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
|
||||
@ -125,7 +125,6 @@
|
||||
* GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/dromara/RuoYi-Vue-Plus)
|
||||
* 单模块 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/fast/)
|
||||
* 微服务 分支 [RuoYi-Cloud-Plus](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus)
|
||||
* Vue3 分支 [RuoYi-Vue-Plus-UI](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI)
|
||||
* 用户扩展项目 [扩展项目列表](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4478302&doc_id=1469725)
|
||||
|
||||
## 加群与捐献
|
||||
|
34
pom.xml
@ -6,15 +6,15 @@
|
||||
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
|
||||
<name>RuoYi-Vue-Plus</name>
|
||||
<url>https://gitee.com/dromara/RuoYi-Vue-Plus</url>
|
||||
<description>RuoYi-Vue-Plus后台管理系统</description>
|
||||
|
||||
<properties>
|
||||
<ruoyi-vue-plus.version>4.7.0</ruoyi-vue-plus.version>
|
||||
<spring-boot.version>2.7.11</spring-boot.version>
|
||||
<ruoyi-vue-plus.version>4.8.0</ruoyi-vue-plus.version>
|
||||
<spring-boot.version>2.7.13</spring-boot.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
@ -22,9 +22,9 @@
|
||||
<spring-boot.mybatis>2.2.2</spring-boot.mybatis>
|
||||
<springdoc.version>1.6.15</springdoc.version>
|
||||
<poi.version>5.2.3</poi.version>
|
||||
<easyexcel.version>3.2.1</easyexcel.version>
|
||||
<easyexcel.version>3.3.1</easyexcel.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<satoken.version>1.34.0</satoken.version>
|
||||
<satoken.version>1.35.0.RC</satoken.version>
|
||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<hutool.version>5.8.18</hutool.version>
|
||||
@ -46,8 +46,7 @@
|
||||
<!-- OSS 配置 -->
|
||||
<aws-java-sdk-s3.version>1.12.400</aws-java-sdk-s3.version>
|
||||
<!-- SMS 配置 -->
|
||||
<aliyun.sms.version>2.0.23</aliyun.sms.version>
|
||||
<tencent.sms.version>3.1.687</tencent.sms.version>
|
||||
<sms4j.version>2.2.0</sms4j.version>
|
||||
</properties>
|
||||
|
||||
<profiles>
|
||||
@ -200,16 +199,11 @@
|
||||
<version>${aws-java-sdk-s3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--短信sms4j-->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<version>${aliyun.sms.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<version>${tencent.sms.version}</version>
|
||||
<groupId>org.dromara.sms4j</groupId>
|
||||
<artifactId>sms4j-spring-boot-starter</artifactId>
|
||||
<version>${sms4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@ -420,8 +414,8 @@
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<name>huawei nexus</name>
|
||||
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
@ -431,8 +425,8 @@
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<name>huawei nexus</name>
|
||||
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
|
@ -16,12 +16,13 @@ import com.ruoyi.common.utils.reflect.ReflectUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.framework.config.properties.CaptchaProperties;
|
||||
import com.ruoyi.framework.config.properties.MailProperties;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.sms4j.provider.enumerate.SupplierType;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -47,7 +49,6 @@ import java.util.Map;
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final SmsProperties smsProperties;
|
||||
private final ISysConfigService configService;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
@ -58,21 +59,18 @@ public class CaptchaController {
|
||||
*/
|
||||
@GetMapping("/captchaSms")
|
||||
public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
||||
if (!smsProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启短信功能!");
|
||||
}
|
||||
String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "";
|
||||
Map<String, String> map = new HashMap<>(1);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
|
||||
SmsResult result = smsTemplate.send(phonenumber, templateId, map);
|
||||
if (!result.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", result);
|
||||
return R.fail(result.getMessage());
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
|
||||
if (!"OK".equals(smsResponse.getCode())) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(smsResponse.getMessage());
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ public class SysUserOnlineController extends BaseController {
|
||||
for (String key : keys) {
|
||||
String token = StringUtils.substringAfterLast(key, ":");
|
||||
// 如果已经过期则跳过
|
||||
if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
|
||||
if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
|
||||
continue;
|
||||
}
|
||||
userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));
|
||||
|
@ -80,6 +80,7 @@ public class SysRoleController extends BaseController {
|
||||
@Log(title = "角色管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody SysRole role) {
|
||||
roleService.checkRoleAllowed(role);
|
||||
if (!roleService.checkRoleNameUnique(role)) {
|
||||
return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
|
||||
} else if (!roleService.checkRoleKeyUnique(role)) {
|
||||
|
@ -158,14 +158,29 @@ mail:
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
|
||||
sms:
|
||||
enabled: false
|
||||
# 阿里云 dysmsapi.aliyuncs.com
|
||||
# 腾讯云 sms.tencentcloudapi.com
|
||||
endpoint: "dysmsapi.aliyuncs.com"
|
||||
alibaba:
|
||||
#请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
|
||||
requestUrl: dysmsapi.aliyuncs.com
|
||||
#阿里云的accessKey
|
||||
accessKeyId: xxxxxxx
|
||||
accessKeySecret: xxxxxx
|
||||
signName: 测试
|
||||
# 腾讯专用
|
||||
sdkAppId:
|
||||
#阿里云的accessKeySecret
|
||||
accessKeySecret: xxxxxxx
|
||||
#短信签名
|
||||
signature: 测试
|
||||
tencent:
|
||||
#请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
|
||||
requestUrl: sms.tencentcloudapi.com
|
||||
#腾讯云的accessKey
|
||||
accessKeyId: xxxxxxx
|
||||
#腾讯云的accessKeySecret
|
||||
accessKeySecret: xxxxxxx
|
||||
#短信签名
|
||||
signature: 测试
|
||||
#短信sdkAppId
|
||||
sdkAppId: appid
|
||||
#地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
|
||||
territory: ap-guangzhou
|
||||
|
@ -161,14 +161,29 @@ mail:
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
|
||||
sms:
|
||||
enabled: false
|
||||
# 阿里云 dysmsapi.aliyuncs.com
|
||||
# 腾讯云 sms.tencentcloudapi.com
|
||||
endpoint: "dysmsapi.aliyuncs.com"
|
||||
alibaba:
|
||||
#请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
|
||||
requestUrl: dysmsapi.aliyuncs.com
|
||||
#阿里云的accessKey
|
||||
accessKeyId: xxxxxxx
|
||||
accessKeySecret: xxxxxx
|
||||
signName: 测试
|
||||
# 腾讯专用
|
||||
sdkAppId:
|
||||
#阿里云的accessKeySecret
|
||||
accessKeySecret: xxxxxxx
|
||||
#短信签名
|
||||
signature: 测试
|
||||
tencent:
|
||||
#请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
|
||||
requestUrl: sms.tencentcloudapi.com
|
||||
#腾讯云的accessKey
|
||||
accessKeyId: xxxxxxx
|
||||
#腾讯云的accessKeySecret
|
||||
accessKeySecret: xxxxxxx
|
||||
#短信签名
|
||||
signature: 测试
|
||||
#短信sdkAppId
|
||||
sdkAppId: appid
|
||||
#地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
|
||||
territory: ap-guangzhou
|
||||
|
@ -104,8 +104,9 @@ sa-token:
|
||||
token-name: Authorization
|
||||
# token有效期 设为一天 (必定过期) 单位: 秒
|
||||
timeout: 86400
|
||||
# token临时有效期 (指定时间无操作就过期) 单位: 秒
|
||||
activity-timeout: 1800
|
||||
# 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义
|
||||
# token最低活跃时间 (指定时间无操作就过期) 单位: 秒
|
||||
active-timeout: 1800
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
is-concurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
@ -32,7 +32,7 @@ public @interface EncryptField {
|
||||
String publicKey() default "";
|
||||
|
||||
/**
|
||||
* 公钥。RSA、SM2需要
|
||||
* 私钥。RSA、SM2需要
|
||||
*/
|
||||
String privateKey() default "";
|
||||
|
||||
|
@ -129,4 +129,9 @@ public interface UserConstants {
|
||||
*/
|
||||
Long ADMIN_ID = 1L;
|
||||
|
||||
/**
|
||||
* 管理员角色key
|
||||
*/
|
||||
String ADMIN_ROLE_KEY = "admin";
|
||||
|
||||
}
|
||||
|
@ -37,14 +37,36 @@ public class ExcelEnumConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
Object codeValue = cellData.getData();
|
||||
cellData.checkEmpty();
|
||||
// Excel中填入的是枚举中指定的描述
|
||||
Object textValue = null;
|
||||
switch (cellData.getType()) {
|
||||
case STRING:
|
||||
case DIRECT_STRING:
|
||||
case RICH_TEXT_STRING:
|
||||
textValue = cellData.getStringValue();
|
||||
break;
|
||||
case NUMBER:
|
||||
textValue = cellData.getNumberValue();
|
||||
break;
|
||||
case BOOLEAN:
|
||||
textValue = cellData.getBooleanValue();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("单元格类型异常!");
|
||||
}
|
||||
// 如果是空值
|
||||
if (ObjectUtil.isNull(codeValue)) {
|
||||
if (ObjectUtil.isNull(textValue)) {
|
||||
return null;
|
||||
}
|
||||
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
|
||||
String textValue = enumValueMap.get(codeValue);
|
||||
return Convert.convert(contentProperty.getField().getType(), textValue);
|
||||
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
|
||||
// 从Java输出至Excel是code转text
|
||||
// 因此从Excel转Java应该将text与code对调
|
||||
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
|
||||
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
|
||||
// 应该从text -> code中查找
|
||||
Object codeValue = enumTextToCodeMap.get(textValue);
|
||||
return Convert.convert(contentProperty.getField().getType(), codeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,6 +53,7 @@ public class SysUser extends BaseEntity {
|
||||
* 用户昵称
|
||||
*/
|
||||
@Xss(message = "用户昵称不能包含脚本字符")
|
||||
@NotBlank(message = "用户昵称不能为空")
|
||||
@Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
|
||||
private String nickName;
|
||||
|
||||
|
@ -6,7 +6,7 @@ import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 短信登录对象
|
||||
* 邮箱登录对象
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.ruoyi.common.core.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通用 字典服务
|
||||
*
|
||||
@ -54,4 +56,11 @@ public interface DictService {
|
||||
*/
|
||||
String getDictValue(String dictType, String dictLabel, String separator);
|
||||
|
||||
/**
|
||||
* 获取字典下所有的字典值与标签
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return dictValue为key,dictLabel为值组成的Map
|
||||
*/
|
||||
Map<String, String> getAllDictByDictType(String dictType);
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.metadata.Head;
|
||||
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
|
||||
import com.ruoyi.common.annotation.CellMerge;
|
||||
import com.ruoyi.common.utils.reflect.ReflectUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -24,20 +25,26 @@ import java.util.Map;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy {
|
||||
|
||||
private List<?> list;
|
||||
private boolean hasTitle;
|
||||
private final List<CellRangeAddress> cellList;
|
||||
private final boolean hasTitle;
|
||||
private int rowIndex;
|
||||
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle) {
|
||||
this.hasTitle = hasTitle;
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
this.cellList = handle(list, hasTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
List<CellRangeAddress> cellList = handle(list, hasTitle);
|
||||
// judge the list is not null
|
||||
if (CollectionUtils.isNotEmpty(cellList)) {
|
||||
if (CollUtil.isNotEmpty(cellList)) {
|
||||
// the judge is necessary
|
||||
if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
|
||||
if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
|
||||
for (CellRangeAddress item : cellList) {
|
||||
sheet.addMergedRegion(item);
|
||||
}
|
||||
@ -46,13 +53,13 @@ public class CellMergeStrategy extends AbstractMergeStrategy {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return cellList;
|
||||
}
|
||||
Class<?> clazz = list.get(0).getClass();
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
|
||||
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> mergeFieldsIndex = new ArrayList<>();
|
||||
@ -62,19 +69,19 @@ public class CellMergeStrategy extends AbstractMergeStrategy {
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
mergeFields.add(field);
|
||||
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
}
|
||||
// 行合并开始下标
|
||||
int rowIndex = hasTitle ? 1 : 0;
|
||||
}
|
||||
|
||||
Map<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
String name = field.getName();
|
||||
String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
Method readMethod = clazz.getMethod(methodName);
|
||||
Object val = readMethod.invoke(list.get(i));
|
||||
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
|
||||
|
||||
int colNum = mergeFieldsIndex.get(j);
|
||||
if (!map.containsKey(field)) {
|
||||
|
@ -0,0 +1,149 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <h1>Excel下拉可选项</h1>
|
||||
* 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unused")
|
||||
public class DropDownOptions {
|
||||
/**
|
||||
* 一级下拉所在列index,从0开始算
|
||||
*/
|
||||
private int index = 0;
|
||||
/**
|
||||
* 二级下拉所在的index,从0开始算,不能与一级相同
|
||||
*/
|
||||
private int nextIndex = 0;
|
||||
/**
|
||||
* 一级下拉所包含的数据
|
||||
*/
|
||||
private List<String> options = new ArrayList<>();
|
||||
/**
|
||||
* 二级下拉所包含的数据Map
|
||||
* <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
|
||||
*/
|
||||
private Map<String, List<String>> nextOptions = new HashMap<>();
|
||||
/**
|
||||
* 分隔符
|
||||
*/
|
||||
private static final String DELIMITER = "_";
|
||||
|
||||
/**
|
||||
* 创建只有一级的下拉选
|
||||
*/
|
||||
public DropDownOptions(int index, List<String> options) {
|
||||
this.index = index;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>创建每个选项可选值</h2>
|
||||
* <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
|
||||
*
|
||||
* @param vars 可选值内包含的参数
|
||||
* @return 合规的可选值
|
||||
*/
|
||||
public static String createOptionValue(Object... vars) {
|
||||
StringBuilder stringBuffer = new StringBuilder();
|
||||
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
|
||||
for (int i = 0; i < vars.length; i++) {
|
||||
String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
|
||||
if (!var.matches(regex)) {
|
||||
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
|
||||
}
|
||||
stringBuffer.append(var);
|
||||
if (i < vars.length - 1) {
|
||||
// 直至最后一个前,都以_作为切割线
|
||||
stringBuffer.append(DELIMITER);
|
||||
}
|
||||
}
|
||||
if (stringBuffer.toString().matches("^\\d_*$")) {
|
||||
throw new ServiceException("禁止以数字开头");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将处理后合理的可选值解析为原始的参数
|
||||
*
|
||||
* @param option 经过处理后的合理的可选项
|
||||
* @return 原始的参数
|
||||
*/
|
||||
public static List<String> analyzeOptionValue(String option) {
|
||||
return StrUtil.split(option, DELIMITER, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建级联下拉选项
|
||||
*
|
||||
* @param parentList 父实体可选项原始数据
|
||||
* @param parentIndex 父下拉选位置
|
||||
* @param sonList 子实体可选项原始数据
|
||||
* @param sonIndex 子下拉选位置
|
||||
* @param parentHowToGetIdFunction 父类如何获取唯一标识
|
||||
* @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
|
||||
* @param howToBuildEveryOption 如何生成下拉选内容
|
||||
* @return 级联下拉选项
|
||||
*/
|
||||
public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
|
||||
int parentIndex,
|
||||
List<T> sonList,
|
||||
int sonIndex,
|
||||
Function<T, Number> parentHowToGetIdFunction,
|
||||
Function<T, Number> sonHowToGetParentIdFunction,
|
||||
Function<T, String> howToBuildEveryOption) {
|
||||
DropDownOptions parentLinkSonOptions = new DropDownOptions();
|
||||
// 先创建父类的下拉
|
||||
parentLinkSonOptions.setIndex(parentIndex);
|
||||
parentLinkSonOptions.setOptions(
|
||||
parentList.stream()
|
||||
.map(howToBuildEveryOption)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
// 提取父-子级联下拉
|
||||
Map<String, List<String>> sonOptions = new HashMap<>();
|
||||
// 父级依据自己的ID分组
|
||||
Map<Number, List<T>> parentGroupByIdMap =
|
||||
parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
|
||||
// 遍历每个子集,提取到Map中
|
||||
sonList.forEach(everySon -> {
|
||||
if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
|
||||
// 找到对应的上级
|
||||
T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
|
||||
// 提取名称和ID作为Key
|
||||
String key = howToBuildEveryOption.apply(parentObj);
|
||||
// Key对应的Value
|
||||
List<String> thisParentSonOptionList;
|
||||
if (sonOptions.containsKey(key)) {
|
||||
thisParentSonOptionList = sonOptions.get(key);
|
||||
} else {
|
||||
thisParentSonOptionList = new ArrayList<>();
|
||||
sonOptions.put(key, thisParentSonOptionList);
|
||||
}
|
||||
// 往Value中添加当前子集选项
|
||||
thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
|
||||
}
|
||||
});
|
||||
parentLinkSonOptions.setNextIndex(sonIndex);
|
||||
parentLinkSonOptions.setNextOptions(sonOptions);
|
||||
return parentLinkSonOptions;
|
||||
}
|
||||
}
|
@ -0,0 +1,370 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.EnumUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.write.handler.SheetWriteHandler;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.annotation.ExcelEnumFormat;
|
||||
import com.ruoyi.common.core.service.DictService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.ss.util.WorkbookUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <h1>Excel表格下拉选操作</h1>
|
||||
* 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
|
||||
* <p>
|
||||
* 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelDownHandler implements SheetWriteHandler {
|
||||
|
||||
/**
|
||||
* Excel表格中的列名英文
|
||||
* 仅为了解析列英文,禁止修改
|
||||
*/
|
||||
private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/**
|
||||
* 单选数据Sheet名
|
||||
*/
|
||||
private static final String OPTIONS_SHEET_NAME = "options";
|
||||
/**
|
||||
* 联动选择数据Sheet名的头
|
||||
*/
|
||||
private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
|
||||
/**
|
||||
* 下拉可选项
|
||||
*/
|
||||
private final List<DropDownOptions> dropDownOptions;
|
||||
/**
|
||||
* 当前单选进度
|
||||
*/
|
||||
private int currentOptionsColumnIndex;
|
||||
/**
|
||||
* 当前联动选择进度
|
||||
*/
|
||||
private int currentLinkedOptionsSheetIndex;
|
||||
private final DictService dictService;
|
||||
|
||||
public ExcelDownHandler(List<DropDownOptions> options) {
|
||||
this.dropDownOptions = options;
|
||||
this.currentOptionsColumnIndex = 0;
|
||||
this.currentLinkedOptionsSheetIndex = 0;
|
||||
this.dictService = SpringUtils.getBean(DictService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>开始创建下拉数据</h2>
|
||||
* 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
|
||||
* 如果有且设置了value值,则将其直接置为下拉可选项
|
||||
* <p>
|
||||
* 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
|
||||
* <p>
|
||||
* 3.二者并存,注意调用方式
|
||||
*/
|
||||
@Override
|
||||
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
|
||||
Sheet sheet = writeSheetHolder.getSheet();
|
||||
// 开始设置下拉框 HSSFWorkbook
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
|
||||
Workbook workbook = writeWorkbookHolder.getWorkbook();
|
||||
int length = fields.length;
|
||||
for (int i = 0; i < length; i++) {
|
||||
// 循环实体中的每个属性
|
||||
// 可选的下拉值
|
||||
List<String> options = new ArrayList<>();
|
||||
if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
|
||||
// 如果指定了@ExcelDictFormat,则使用字典的逻辑
|
||||
ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
|
||||
String dictType = format.dictType();
|
||||
String converterExp = format.readConverterExp();
|
||||
if (StrUtil.isNotBlank(dictType)) {
|
||||
// 如果传递了字典名,则依据字典建立下拉
|
||||
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
|
||||
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
|
||||
.values();
|
||||
options = new ArrayList<>(values);
|
||||
} else if (StrUtil.isNotBlank(converterExp)) {
|
||||
// 如果指定了确切的值,则直接解析确切的值
|
||||
options = StrUtil.split(converterExp, format.separator(), true, true);
|
||||
}
|
||||
} else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
|
||||
// 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
|
||||
ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||
options = StreamUtils.toList(values, String::valueOf);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(options)) {
|
||||
// 仅当下拉可选项不为空时执行
|
||||
// 获取列下标,默认为当前循环次数
|
||||
int index = i;
|
||||
if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
|
||||
// 如果指定了列下标,以指定的为主
|
||||
index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
|
||||
}
|
||||
if (options.size() > 20) {
|
||||
// 这里限制如果可选项大于20,则使用额外表形式
|
||||
dropDownWithSheet(helper, workbook, sheet, index, options);
|
||||
} else {
|
||||
// 否则使用固定值形式
|
||||
dropDownWithSimple(helper, sheet, index, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
dropDownOptions.forEach(everyOptions -> {
|
||||
// 如果传递了下拉框选择器参数
|
||||
if (!everyOptions.getNextOptions().isEmpty()) {
|
||||
// 当二级选项不为空时,使用额外关联表的形式
|
||||
dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
|
||||
} else if (everyOptions.getOptions().size() > 10) {
|
||||
// 当一级选项参数个数大于10,使用额外表的形式
|
||||
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
} else if (everyOptions.getOptions().size() != 0) {
|
||||
// 当一级选项个数不为空,使用默认形式
|
||||
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>简单下拉框</h2>
|
||||
* 直接将可选项拼接为指定列的数据校验值
|
||||
*
|
||||
* @param celIndex 列index
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
if (ObjectUtil.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的级联下拉框</h2>
|
||||
*
|
||||
* @param options 额外表格形式存储的下拉可选项
|
||||
*/
|
||||
private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
|
||||
String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
|
||||
// 创建联动下拉数据表
|
||||
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
|
||||
// 完善横向的一级选项数据表
|
||||
List<String> firstOptions = options.getOptions();
|
||||
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
name.setNameName(linkedOptionsSheetName);
|
||||
// 以横向第一行创建一级下拉拼接引用位置
|
||||
String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
|
||||
linkedOptionsSheetName,
|
||||
getExcelColumnName(0),
|
||||
getExcelColumnName(firstOptions.size())
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(firstOptionsFunction);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
|
||||
|
||||
for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
|
||||
// 先提取主表中一级下拉的列名
|
||||
String firstOptionsColumnName = getExcelColumnName(columIndex);
|
||||
// 一次循环是每一个一级选项
|
||||
int finalI = columIndex;
|
||||
// 本次循环的一级选项值
|
||||
String thisFirstOptionsValue = firstOptions.get(columIndex);
|
||||
// 创建第一行的数据
|
||||
Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
|
||||
// 如果不存在则创建第一行
|
||||
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
|
||||
// 第一行当前列
|
||||
.createCell(columIndex)
|
||||
// 设置值为当前一级选项值
|
||||
.setCellValue(thisFirstOptionsValue);
|
||||
|
||||
// 第二行开始,设置第二级别选项参数
|
||||
List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
|
||||
if (CollUtil.isEmpty(secondOptions)) {
|
||||
// 必须保证至少有一个关联选项,否则将导致Excel解析错误
|
||||
secondOptions = Collections.singletonList("暂无_0");
|
||||
}
|
||||
|
||||
// 以该一级选项值创建子名称管理器
|
||||
Name sonName = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
sonName.setNameName(thisFirstOptionsValue);
|
||||
// 以第二行该列数据拼接引用位置
|
||||
String sonFunction = String.format("%s!$%s$2:$%s$%d",
|
||||
linkedOptionsSheetName,
|
||||
firstOptionsColumnName,
|
||||
firstOptionsColumnName,
|
||||
secondOptions.size() + 1
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
sonName.setRefersToFormula(sonFunction);
|
||||
// 数据验证为序列模式,引用到每一个主表中的二级选项位置
|
||||
// 创建子项的名称管理器,只是为了使得Excel可以识别到数据
|
||||
String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// 以一级选项对应的主体所在位置创建二级下拉
|
||||
String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
|
||||
// 二级只能主表每一行的每一列添加二级校验
|
||||
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
|
||||
}
|
||||
|
||||
for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
|
||||
// 从第二行开始填充二级选项
|
||||
int finalRowIndex = rowIndex + 1;
|
||||
int finalColumIndex = columIndex;
|
||||
|
||||
Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
|
||||
// 没有则创建
|
||||
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
|
||||
Optional
|
||||
// 在本级一级选项所在的列
|
||||
.ofNullable(row.getCell(finalColumIndex))
|
||||
// 不存在则创建
|
||||
.orElseGet(() -> row.createCell(finalColumIndex))
|
||||
// 设置二级选项值
|
||||
.setCellValue(secondOptions.get(rowIndex));
|
||||
}
|
||||
}
|
||||
|
||||
currentLinkedOptionsSheetIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的普通下拉框</h2>
|
||||
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
|
||||
*
|
||||
* @param celIndex 下拉选
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
// 创建下拉数据表
|
||||
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
|
||||
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
|
||||
// 完善纵向的一级选项数据表
|
||||
for (int i = 0; i < value.size(); i++) {
|
||||
int finalI = i;
|
||||
// 获取每一选项行,如果没有则创建
|
||||
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
|
||||
.orElseGet(() -> simpleDataSheet.createRow(finalI));
|
||||
// 获取本级选项对应的选项列,如果没有则创建
|
||||
Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
|
||||
.orElseGet(() -> row.createCell(currentOptionsColumnIndex));
|
||||
// 设置值
|
||||
cell.setCellValue(value.get(i));
|
||||
}
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
|
||||
name.setNameName(nameName);
|
||||
// 以纵向第一列创建一级下拉拼接引用位置
|
||||
String function = String.format("%s!$%s$1:$%s$%d",
|
||||
OPTIONS_SHEET_NAME,
|
||||
getExcelColumnName(currentOptionsColumnIndex),
|
||||
getExcelColumnName(currentOptionsColumnIndex),
|
||||
value.size());
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(function);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
|
||||
currentOptionsColumnIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限一级选项
|
||||
*/
|
||||
private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
|
||||
DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限二级选项
|
||||
*/
|
||||
private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
|
||||
Integer celIndex, DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据校验
|
||||
*/
|
||||
private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
|
||||
DataValidationConstraint constraint, CellRangeAddressList addressList) {
|
||||
// 数据有效性对象
|
||||
DataValidation dataValidation = helper.createValidation(constraint, addressList);
|
||||
// 处理Excel兼容性问题
|
||||
if (dataValidation instanceof XSSFDataValidation) {
|
||||
//数据校验
|
||||
dataValidation.setSuppressDropDownArrow(true);
|
||||
//错误提示
|
||||
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
|
||||
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
|
||||
dataValidation.setShowErrorBox(true);
|
||||
//选定提示
|
||||
dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
|
||||
dataValidation.setShowPromptBox(true);
|
||||
sheet.addValidationData(dataValidation);
|
||||
} else {
|
||||
dataValidation.setSuppressDropDownArrow(false);
|
||||
}
|
||||
sheet.addValidationData(dataValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>依据列index获取列名英文</h2>
|
||||
* 依据列index转换为Excel中的列名英文
|
||||
* <p>例如第1列,index为0,解析出来为A列</p>
|
||||
* 第27列,index为26,解析为AA列
|
||||
* <p>第28列,index为27,解析为AB列</p>
|
||||
*
|
||||
* @param columnIndex 列index
|
||||
* @return 列index所在得英文名
|
||||
*/
|
||||
private String getExcelColumnName(int columnIndex) {
|
||||
// 26一循环的次数
|
||||
int columnCircleCount = columnIndex / 26;
|
||||
// 26一循环内的位置
|
||||
int thisCircleColumnIndex = columnIndex % 26;
|
||||
// 26一循环的次数大于0,则视为栏名至少两位
|
||||
String columnPrefix = columnCircleCount == 0
|
||||
? StrUtil.EMPTY
|
||||
: StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
|
||||
// 从26一循环内取对应的栏位名
|
||||
String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
|
||||
// 将二者拼接即为最终的栏位名
|
||||
return columnPrefix + columnNext;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.ruoyi.common.helper;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
@ -54,6 +55,14 @@ public class LoginHelper {
|
||||
if (ObjectUtil.isNotNull(deviceType)) {
|
||||
model.setDevice(deviceType.getDevice());
|
||||
}
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
// UserType userType = UserType.getUserType(loginUser.getUserType());
|
||||
// if (userType == UserType.SYS_USER) {
|
||||
// model.setTimeout(86400).setActiveTimeout(1800);
|
||||
// } else if (userType == UserType.APP_USER) {
|
||||
// model.setTimeout(86400).setActiveTimeout(1800);
|
||||
// }
|
||||
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
|
||||
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
|
||||
}
|
||||
@ -66,7 +75,11 @@ public class LoginHelper {
|
||||
if (loginUser != null) {
|
||||
return loginUser;
|
||||
}
|
||||
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
|
||||
SaSession session = StpUtil.getTokenSession();
|
||||
if (ObjectUtil.isNull(session)) {
|
||||
return null;
|
||||
}
|
||||
loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
return loginUser;
|
||||
}
|
||||
@ -75,7 +88,11 @@ public class LoginHelper {
|
||||
* 获取用户基于token
|
||||
*/
|
||||
public static LoginUser getLoginUser(String token) {
|
||||
return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
|
||||
SaSession session = StpUtil.getTokenSessionByToken(token);
|
||||
if (ObjectUtil.isNull(session)) {
|
||||
return null;
|
||||
}
|
||||
return (LoginUser) session.get(LOGIN_USER_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,8 +130,8 @@ public class LoginHelper {
|
||||
* 获取用户类型
|
||||
*/
|
||||
public static UserType getUserType() {
|
||||
String loginId = StpUtil.getLoginIdAsString();
|
||||
return UserType.getUserType(loginId);
|
||||
String loginType = StpUtil.getLoginIdAsString();
|
||||
return UserType.getUserType(loginType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,11 +17,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* bean深拷贝工具(基于 cglib 性能优异)
|
||||
* bean拷贝工具(基于 cglib 性能优异)
|
||||
* <p>
|
||||
* 重点 cglib 不支持 拷贝到链式对象
|
||||
* 例如: 源对象 拷贝到 目标(链式对象)
|
||||
* 请区分好`浅拷贝`和`深拷贝`再做使用
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
@ -30,6 +30,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(function).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -70,7 +71,8 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return collection.stream().sorted(comparing).collect(Collectors.toList());
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +89,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +108,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,7 +126,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
@ -145,7 +147,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
|
||||
}
|
||||
|
||||
@ -166,7 +168,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
|
||||
}
|
||||
|
||||
@ -188,6 +190,7 @@ public class StreamUtils {
|
||||
.stream()
|
||||
.map(function)
|
||||
.filter(Objects::nonNull)
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig;
|
||||
import com.alibaba.excel.write.metadata.fill.FillWrapper;
|
||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import com.ruoyi.common.convert.ExcelBigNumberConvert;
|
||||
import com.ruoyi.common.excel.CellMergeStrategy;
|
||||
import com.ruoyi.common.excel.DefaultExcelListener;
|
||||
import com.ruoyi.common.excel.ExcelListener;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.excel.*;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import lombok.AccessLevel;
|
||||
@ -88,7 +85,26 @@ public class ExcelUtil {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os);
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
@ -107,7 +123,27 @@ public class ExcelUtil {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os);
|
||||
exportExcel(list, sheetName, clazz, merge, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param merge 是否合并单元格
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
@ -122,7 +158,20 @@ public class ExcelUtil {
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
|
||||
exportExcel(list, sheetName, clazz, false, os);
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param os 输出流
|
||||
* @param options 级联下拉选内容
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,7 +183,8 @@ public class ExcelUtil {
|
||||
* @param merge 是否合并单元格
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
|
||||
OutputStream os, List<DropDownOptions> options) {
|
||||
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
|
||||
.autoCloseStream(false)
|
||||
// 自动适配
|
||||
@ -146,6 +196,10 @@ public class ExcelUtil {
|
||||
// 合并处理器
|
||||
builder.registerWriteHandler(new CellMergeStrategy(list, true));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(options)) {
|
||||
// 添加下拉框操作
|
||||
builder.registerWriteHandler(new ExcelDownHandler(options));
|
||||
}
|
||||
builder.doWrite(list);
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,18 @@ public class RedisUtils {
|
||||
batch.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果不存在则设置 并返回 true 如果存在则返回 false
|
||||
*
|
||||
* @param key 缓存的键值
|
||||
* @param value 缓存的值
|
||||
* @return set成功或失败
|
||||
*/
|
||||
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
|
||||
RBucket<T> bucket = CLIENT.getBucket(key);
|
||||
return bucket.setIfAbsent(value, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册对象监听器
|
||||
* <p>
|
||||
@ -374,6 +386,21 @@ public class RedisUtils {
|
||||
return rMap.remove(hKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash中的数据
|
||||
*
|
||||
* @param key Redis键
|
||||
* @param hKeys Hash键
|
||||
*/
|
||||
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
|
||||
RBatch batch = CLIENT.createBatch();
|
||||
RMapAsync<String, T> rMap = batch.getMap(key);
|
||||
for (String hKey : hKeys) {
|
||||
rMap.removeAsync(hKey);
|
||||
}
|
||||
batch.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个Hash中的数据
|
||||
*
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@ -28,17 +28,6 @@
|
||||
<artifactId>ruoyi-sms</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 短信 用哪个导入哪个依赖 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.aliyun</groupId>-->
|
||||
<!-- <artifactId>dysmsapi20170525</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.tencentcloudapi</groupId>-->
|
||||
<!-- <artifactId>tencentcloud-sdk-java-sms</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -1,17 +1,17 @@
|
||||
package com.ruoyi.demo.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.sms4j.provider.enumerate.SupplierType;
|
||||
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;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 短信演示案例
|
||||
@ -26,10 +26,6 @@ import java.util.Map;
|
||||
@RequestMapping("/demo/sms")
|
||||
public class SmsController {
|
||||
|
||||
private final SmsProperties smsProperties;
|
||||
// private final SmsTemplate smsTemplate; // 可以使用spring注入
|
||||
// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
|
||||
|
||||
/**
|
||||
* 发送短信Aliyun
|
||||
*
|
||||
@ -38,17 +34,11 @@ public class SmsController {
|
||||
*/
|
||||
@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);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", "1234");
|
||||
Object send = smsTemplate.send(phones, templateId, map);
|
||||
return R.ok(send);
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
|
||||
return R.ok(smsResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,18 +49,12 @@ public class SmsController {
|
||||
*/
|
||||
@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);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
// map.put("2", "测试测试");
|
||||
map.put("1", "1234");
|
||||
Object send = smsTemplate.send(phones, templateId, map);
|
||||
return R.ok(send);
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
|
||||
return R.ok(smsResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
package com.ruoyi.demo.controller;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
import com.ruoyi.demo.listener.ExportDemoListener;
|
||||
import com.ruoyi.demo.service.IExportExcelService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
@ -20,9 +25,12 @@ import java.util.Map;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/demo/excel")
|
||||
public class TestExcelController {
|
||||
|
||||
private final IExportExcelService exportExcelService;
|
||||
|
||||
/**
|
||||
* 单列表多数据
|
||||
*/
|
||||
@ -76,6 +84,26 @@ public class TestExcelController {
|
||||
ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出下拉框
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
@GetMapping("/exportWithOptions")
|
||||
public void exportWithOptions(HttpServletResponse response) {
|
||||
exportExcelService.exportWithOptions(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入表格
|
||||
*/
|
||||
@PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public List<ExportDemoVo> importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
|
||||
// 处理解析结果
|
||||
ExcelResult<ExportDemoVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
|
||||
return excelResult.getList();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class TestObj1 {
|
||||
|
@ -0,0 +1,119 @@
|
||||
package com.ruoyi.demo.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.annotation.ExcelEnumFormat;
|
||||
import com.ruoyi.common.convert.ExcelDictConvert;
|
||||
import com.ruoyi.common.convert.ExcelEnumConvert;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 带有下拉选的Excel导出
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ExportDemoVo {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@ExcelProperty(value = "用户名", index = 0)
|
||||
@NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
* </p>
|
||||
* 使用ExcelEnumFormat注解需要进行下拉选的部分
|
||||
*/
|
||||
@ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
|
||||
@ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
|
||||
@NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
|
||||
private String userStatus;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
* <p>
|
||||
* 使用ExcelDictFormat注解需要进行下拉选的部分
|
||||
*/
|
||||
@ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_user_sex")
|
||||
@NotEmpty(message = "性别不能为空", groups = AddGroup.class)
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@ExcelProperty(value = "手机号", index = 3)
|
||||
@NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* Email
|
||||
*/
|
||||
@ExcelProperty(value = "Email", index = 4)
|
||||
@NotEmpty(message = "Email不能为空", groups = AddGroup.class)
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 省
|
||||
* <p>
|
||||
* 级联下拉,仅判断是否选了
|
||||
*/
|
||||
@ExcelProperty(value = "省", index = 5)
|
||||
@NotNull(message = "省不能为空", groups = AddGroup.class)
|
||||
private String province;
|
||||
|
||||
/**
|
||||
* 数据库中的省ID
|
||||
* </p>
|
||||
* 处理完毕后再判断是否市正确的值
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer provinceId;
|
||||
|
||||
/**
|
||||
* 市
|
||||
* <p>
|
||||
* 级联下拉
|
||||
*/
|
||||
@ExcelProperty(value = "市", index = 6)
|
||||
@NotNull(message = "市不能为空", groups = AddGroup.class)
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 数据库中的市ID
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer cityId;
|
||||
|
||||
/**
|
||||
* 县
|
||||
* <p>
|
||||
* 级联下拉
|
||||
*/
|
||||
@ExcelProperty(value = "县", index = 7)
|
||||
@NotNull(message = "县不能为空", groups = AddGroup.class)
|
||||
private String area;
|
||||
|
||||
/**
|
||||
* 数据库中的县ID
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer areaId;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.ruoyi.demo.listener;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.excel.DefaultExcelListener;
|
||||
import com.ruoyi.common.excel.DropDownOptions;
|
||||
import com.ruoyi.common.utils.ValidatorUtils;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Excel带下拉框的解析处理器
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
|
||||
|
||||
public ExportDemoListener() {
|
||||
// 显示使用构造函数,否则将导致空指针
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(ExportDemoVo data, AnalysisContext context) {
|
||||
// 先校验必填
|
||||
ValidatorUtils.validate(data, AddGroup.class);
|
||||
|
||||
// 处理级联下拉的部分
|
||||
String province = data.getProvince();
|
||||
String city = data.getCity();
|
||||
String area = data.getArea();
|
||||
// 本行用户选择的省
|
||||
List<String> thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
|
||||
if (thisRowSelectedProvinceOption.size() == 2) {
|
||||
String provinceIdStr = thisRowSelectedProvinceOption.get(1);
|
||||
if (NumberUtil.isNumber(provinceIdStr)) {
|
||||
// 严格要求数据的话可以在这里做与数据库相关的判断
|
||||
// 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
|
||||
data.setProvinceId(Integer.parseInt(provinceIdStr));
|
||||
}
|
||||
}
|
||||
// 本行用户选择的市
|
||||
List<String> thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
|
||||
if (thisRowSelectedCityOption.size() == 2) {
|
||||
String cityIdStr = thisRowSelectedCityOption.get(1);
|
||||
if (NumberUtil.isNumber(cityIdStr)) {
|
||||
data.setCityId(Integer.parseInt(cityIdStr));
|
||||
}
|
||||
}
|
||||
// 本行用户选择的县
|
||||
List<String> thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
|
||||
if (thisRowSelectedAreaOption.size() == 2) {
|
||||
String areaIdStr = thisRowSelectedAreaOption.get(1);
|
||||
if (NumberUtil.isNumber(areaIdStr)) {
|
||||
data.setAreaId(Integer.parseInt(areaIdStr));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理完毕以后判断是否符合规则
|
||||
ValidatorUtils.validate(data, EditGroup.class);
|
||||
|
||||
// 添加到处理结果中
|
||||
getExcelResult().getList().add(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ruoyi.demo.service;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 导出下拉框Excel示例
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
public interface IExportExcelService {
|
||||
|
||||
/**
|
||||
* 导出下拉框
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
void exportWithOptions(HttpServletResponse response);
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
package com.ruoyi.demo.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
import com.ruoyi.common.excel.DropDownOptions;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
import com.ruoyi.demo.service.IExportExcelService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 导出下拉框Excel示例
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
|
||||
@Override
|
||||
public void exportWithOptions(HttpServletResponse response) {
|
||||
// 创建表格数据,业务中一般通过数据库查询
|
||||
List<ExportDemoVo> excelDataList = new ArrayList<>();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// 模拟数据库中的一条数据
|
||||
ExportDemoVo everyRowData = new ExportDemoVo();
|
||||
everyRowData.setNickName("用户-" + i);
|
||||
everyRowData.setUserStatus(UserStatus.OK.getCode());
|
||||
everyRowData.setGender("1");
|
||||
everyRowData.setPhoneNumber(String.format("175%08d", i));
|
||||
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
|
||||
everyRowData.setProvinceId(i);
|
||||
everyRowData.setCityId(i);
|
||||
everyRowData.setAreaId(i);
|
||||
excelDataList.add(everyRowData);
|
||||
}
|
||||
|
||||
// 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
|
||||
// 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
|
||||
// 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
|
||||
|
||||
// 首先从数据库中查询下拉框内的可选项
|
||||
// 这里模拟查询结果
|
||||
List<DemoCityData> provinceList = getProvinceList(),
|
||||
cityList = getCityList(provinceList),
|
||||
areaList = getAreaList(cityList);
|
||||
int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
|
||||
|
||||
DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
|
||||
provinceList,
|
||||
provinceIndex,
|
||||
cityList,
|
||||
cityIndex,
|
||||
DemoCityData::getId,
|
||||
DemoCityData::getPid,
|
||||
everyOptions -> DropDownOptions.createOptionValue(
|
||||
everyOptions.getName(),
|
||||
everyOptions.getId()
|
||||
)
|
||||
);
|
||||
|
||||
DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
|
||||
cityList,
|
||||
cityIndex,
|
||||
areaList,
|
||||
areaIndex,
|
||||
DemoCityData::getId,
|
||||
DemoCityData::getPid,
|
||||
everyOptions -> DropDownOptions.createOptionValue(
|
||||
everyOptions.getName(),
|
||||
everyOptions.getId()
|
||||
)
|
||||
);
|
||||
|
||||
// 把所有的下拉框存储
|
||||
List<DropDownOptions> options = new ArrayList<>();
|
||||
options.add(provinceToCity);
|
||||
options.add(cityToArea);
|
||||
|
||||
// 到此为止所有的下拉框可选项已全部配置完毕
|
||||
|
||||
// 接下来需要将Excel中的展示数据转换为对应的下拉选
|
||||
List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
|
||||
// 只需要处理没有使用@ExcelDictFormat注解的下拉框
|
||||
// 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
|
||||
everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
|
||||
everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
|
||||
everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
|
||||
return everyRowData;
|
||||
});
|
||||
|
||||
ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
|
||||
}
|
||||
|
||||
private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
|
||||
Map<Integer, List<DemoCityData>> groupByIdMap =
|
||||
cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
|
||||
if (groupByIdMap.containsKey(id)) {
|
||||
DemoCityData demoCityData = groupByIdMap.get(id).get(0);
|
||||
return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
|
||||
} else {
|
||||
return StrUtil.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查询数据库操作
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getProvinceList() {
|
||||
List<DemoCityData> provinceList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
provinceList.add(new DemoCityData(0, null, "安徽省"));
|
||||
provinceList.add(new DemoCityData(1, null, "江苏省"));
|
||||
|
||||
return provinceList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查找数据库操作,需要连带查询出省的数据
|
||||
*
|
||||
* @param provinceList 模拟的父省数据
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
|
||||
List<DemoCityData> cityList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
cityList.add(new DemoCityData(0, 0, "合肥市"));
|
||||
cityList.add(new DemoCityData(1, 0, "芜湖市"));
|
||||
cityList.add(new DemoCityData(2, 1, "南京市"));
|
||||
cityList.add(new DemoCityData(3, 1, "无锡市"));
|
||||
cityList.add(new DemoCityData(4, 1, "徐州市"));
|
||||
|
||||
selectParentData(provinceList, cityList);
|
||||
|
||||
return cityList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查找数据库操作,需要连带查询出市的数据
|
||||
*
|
||||
* @param cityList 模拟的父市数据
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
|
||||
List<DemoCityData> areaList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
areaList.add(new DemoCityData(0, 0, "瑶海区"));
|
||||
areaList.add(new DemoCityData(1, 0, "庐江区"));
|
||||
areaList.add(new DemoCityData(2, 1, "南宁县"));
|
||||
areaList.add(new DemoCityData(3, 1, "镜湖区"));
|
||||
areaList.add(new DemoCityData(4, 2, "玄武区"));
|
||||
areaList.add(new DemoCityData(5, 2, "秦淮区"));
|
||||
areaList.add(new DemoCityData(6, 3, "宜兴市"));
|
||||
areaList.add(new DemoCityData(7, 3, "新吴区"));
|
||||
areaList.add(new DemoCityData(8, 4, "鼓楼区"));
|
||||
areaList.add(new DemoCityData(9, 4, "丰县"));
|
||||
|
||||
selectParentData(cityList, areaList);
|
||||
|
||||
return areaList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟数据库的查询父数据操作
|
||||
*
|
||||
* @param parentList /
|
||||
* @param sonList /
|
||||
*/
|
||||
private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
|
||||
Map<Integer, List<DemoCityData>> parentGroupByIdMap =
|
||||
parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
|
||||
|
||||
sonList.forEach(everySon -> {
|
||||
if (parentGroupByIdMap.containsKey(everySon.getPid())) {
|
||||
everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟的数据库省市县
|
||||
*/
|
||||
@Data
|
||||
private static class DemoCityData {
|
||||
/**
|
||||
* 数据库id字段
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 数据库pid字段
|
||||
*/
|
||||
private Integer pid;
|
||||
/**
|
||||
* 数据库name字段
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* MyBatisPlus连带查询父数据
|
||||
*/
|
||||
private DemoCityData pData;
|
||||
|
||||
public DemoCityData(Integer id, Integer pid, String name) {
|
||||
this.id = id;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>ruoyi-extend</artifactId>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-extend</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-extend</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<artifactId>ruoyi-xxl-job-admin</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
@ -2,6 +2,7 @@ package com.ruoyi.framework.aspectj;
|
||||
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.domain.event.OperLogEvent;
|
||||
@ -25,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* 操作日志记录处理
|
||||
@ -144,11 +146,12 @@ public class LogAspect {
|
||||
* 参数拼装
|
||||
*/
|
||||
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
|
||||
StringBuilder params = new StringBuilder();
|
||||
if (paramsArray != null && paramsArray.length > 0) {
|
||||
StringJoiner params = new StringJoiner(" ");
|
||||
if (ArrayUtil.isEmpty(paramsArray)) {
|
||||
return params.toString();
|
||||
}
|
||||
for (Object o : paramsArray) {
|
||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||
try {
|
||||
String str = JsonUtils.toJsonString(o);
|
||||
Dict dict = JsonUtils.parseMap(str);
|
||||
if (MapUtil.isNotEmpty(dict)) {
|
||||
@ -156,14 +159,10 @@ public class LogAspect {
|
||||
MapUtil.removeAny(dict, excludeParamNames);
|
||||
str = JsonUtils.toJsonString(dict);
|
||||
}
|
||||
params.append(str).append(" ");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
params.add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.toString().trim();
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -184,9 +183,8 @@ public class LogAspect {
|
||||
}
|
||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||
Map map = (Map) o;
|
||||
for (Object value : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) value;
|
||||
return entry.getValue() instanceof MultipartFile;
|
||||
for (Object value : map.values()) {
|
||||
return value instanceof MultipartFile;
|
||||
}
|
||||
}
|
||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||
@ -12,8 +13,6 @@ import com.ruoyi.common.utils.MessageUtils;
|
||||
import com.ruoyi.common.utils.ServletUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.redis.RedisUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
@ -28,14 +27,13 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* 防止重复提交(参考美团GTIS防重系统)
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Aspect
|
||||
@Component
|
||||
public class RepeatSubmitAspect {
|
||||
@ -45,10 +43,8 @@ public class RepeatSubmitAspect {
|
||||
@Before("@annotation(repeatSubmit)")
|
||||
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
// 如果注解不为0 则使用注解数值
|
||||
long interval = 0;
|
||||
if (repeatSubmit.interval() > 0) {
|
||||
interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
|
||||
}
|
||||
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
|
||||
|
||||
if (interval < 1000) {
|
||||
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
|
||||
}
|
||||
@ -64,9 +60,7 @@ public class RepeatSubmitAspect {
|
||||
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
|
||||
// 唯一标识(指定key + url + 消息头)
|
||||
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
|
||||
String key = RedisUtils.getCacheObject(cacheRepeatKey);
|
||||
if (key == null) {
|
||||
RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
|
||||
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
|
||||
KEY_CACHE.set(cacheRepeatKey);
|
||||
} else {
|
||||
String message = repeatSubmit.message();
|
||||
@ -114,19 +108,16 @@ public class RepeatSubmitAspect {
|
||||
* 参数拼装
|
||||
*/
|
||||
private String argsArrayToString(Object[] paramsArray) {
|
||||
StringBuilder params = new StringBuilder();
|
||||
if (paramsArray != null && paramsArray.length > 0) {
|
||||
StringJoiner params = new StringJoiner(" ");
|
||||
if (ArrayUtil.isEmpty(paramsArray)) {
|
||||
return params.toString();
|
||||
}
|
||||
for (Object o : paramsArray) {
|
||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||
try {
|
||||
params.append(JsonUtils.toJsonString(o)).append(" ");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
params.add(JsonUtils.toJsonString(o));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.toString().trim();
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -147,9 +138,8 @@ public class RepeatSubmitAspect {
|
||||
}
|
||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||
Map map = (Map) o;
|
||||
for (Object value : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) value;
|
||||
return entry.getValue() instanceof MultipartFile;
|
||||
for (Object value : map.values()) {
|
||||
return value instanceof MultipartFile;
|
||||
}
|
||||
}
|
||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||
|
@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
|
||||
@Override
|
||||
public Cache getCache(String name) {
|
||||
// 重写 cacheName 支持多参数
|
||||
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
||||
name = array[0];
|
||||
|
||||
Cache cache = instanceMap.get(name);
|
||||
if (cache != null) {
|
||||
return cache;
|
||||
@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
configMap.put(name, config);
|
||||
}
|
||||
|
||||
// 重写 cacheName 支持多参数
|
||||
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
||||
name = array[0];
|
||||
if (array.length > 1) {
|
||||
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
@ -108,6 +110,26 @@ public class GlobalExceptionHandler {
|
||||
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求路径中缺少必需的路径变量
|
||||
*/
|
||||
@ExceptionHandler(MissingPathVariableException.class)
|
||||
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
|
||||
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数类型不匹配
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
|
||||
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
@ -24,6 +24,7 @@ import com.ruoyi.oss.exception.OssException;
|
||||
import com.ruoyi.oss.properties.OssProperties;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
@ -115,6 +116,18 @@ public class OssClient {
|
||||
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||
}
|
||||
|
||||
public UploadResult upload(File file, String path) {
|
||||
try {
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
|
||||
// 设置上传对象的 Acl 为公共读
|
||||
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
|
||||
client.putObject(putObjectRequest);
|
||||
} catch (Exception e) {
|
||||
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||
}
|
||||
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||
}
|
||||
|
||||
public void delete(String path) {
|
||||
path = path.replace(getUrl() + "/", "");
|
||||
try {
|
||||
@ -132,6 +145,10 @@ public class OssClient {
|
||||
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
|
||||
}
|
||||
|
||||
public UploadResult uploadSuffix(File file, String suffix) {
|
||||
return upload(file, getPath(properties.getPrefix(), suffix));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元数据
|
||||
*
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@ -24,15 +24,15 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<optional>true</optional>
|
||||
<groupId>org.dromara.sms4j</groupId>
|
||||
<artifactId>sms4j-spring-boot-starter</artifactId>
|
||||
<exclusions>
|
||||
<!-- 排除京东短信内存在的fastjson等待作者后续修复 -->
|
||||
<exclusion>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
@ -1,45 +1,12 @@
|
||||
package com.ruoyi.sms.config;
|
||||
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.AliyunSmsTemplate;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import com.ruoyi.sms.core.TencentSmsTemplate;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 短信配置类
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
@Configuration
|
||||
//@Configuration // 暂时用不上 留着后续扩展使用
|
||||
public class SmsConfig {
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
||||
@ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
|
||||
static class AliyunSmsConfig {
|
||||
|
||||
@Bean
|
||||
public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
|
||||
return new AliyunSmsTemplate(smsProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
||||
@ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
|
||||
static class TencentSmsConfig {
|
||||
|
||||
@Bean
|
||||
public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
|
||||
return new TencentSmsTemplate(smsProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,47 +1,20 @@
|
||||
package com.ruoyi.sms.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* SMS短信 配置属性
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "sms")
|
||||
public class SmsProperties {
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 配置节点
|
||||
* 阿里云 dysmsapi.aliyuncs.com
|
||||
* 腾讯云 sms.tencentcloudapi.com
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* key
|
||||
*/
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* 密匙
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/*
|
||||
* 短信签名
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* 短信应用ID (腾讯专属)
|
||||
*/
|
||||
private String sdkAppId;
|
||||
|
||||
}
|
||||
//package com.ruoyi.sms.config.properties;
|
||||
//
|
||||
//import lombok.Data;
|
||||
//import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
///**
|
||||
// * SMS短信 配置属性
|
||||
// *
|
||||
// * @author Lion Li
|
||||
// * @version 4.2.0
|
||||
// */
|
||||
//@Data
|
||||
//@Component
|
||||
//@ConfigurationProperties(prefix = "sms")
|
||||
//public class SmsProperties {
|
||||
//
|
||||
// private Boolean enabled;
|
||||
//
|
||||
//}
|
||||
|
@ -1,66 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import com.ruoyi.common.utils.JsonUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.sms.exception.SmsException;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Aliyun 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public class AliyunSmsTemplate implements SmsTemplate {
|
||||
|
||||
private SmsProperties properties;
|
||||
|
||||
private Client client;
|
||||
|
||||
@SneakyThrows(Exception.class)
|
||||
public AliyunSmsTemplate(SmsProperties smsProperties) {
|
||||
this.properties = smsProperties;
|
||||
Config config = new Config()
|
||||
// 您的AccessKey ID
|
||||
.setAccessKeyId(smsProperties.getAccessKeyId())
|
||||
// 您的AccessKey Secret
|
||||
.setAccessKeySecret(smsProperties.getAccessKeySecret())
|
||||
// 访问的域名
|
||||
.setEndpoint(smsProperties.getEndpoint());
|
||||
this.client = new Client(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
||||
if (StringUtils.isBlank(phones)) {
|
||||
throw new SmsException("手机号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(templateId)) {
|
||||
throw new SmsException("模板ID不能为空");
|
||||
}
|
||||
SendSmsRequest req = new SendSmsRequest()
|
||||
.setPhoneNumbers(phones)
|
||||
.setSignName(properties.getSignName())
|
||||
.setTemplateCode(templateId)
|
||||
.setTemplateParam(JsonUtils.toJsonString(param));
|
||||
try {
|
||||
SendSmsResponse resp = client.sendSms(req);
|
||||
return SmsResult.builder()
|
||||
.isSuccess("OK".equals(resp.getBody().getCode()))
|
||||
.message(resp.getBody().getMessage())
|
||||
.response(JsonUtils.toJsonString(resp))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new SmsException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public interface SmsTemplate {
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*
|
||||
* @param phones 电话号(多个逗号分割)
|
||||
* @param templateId 模板id
|
||||
* @param param 模板对应参数
|
||||
* 阿里 需使用 模板变量名称对应内容 例如: code=1234
|
||||
* 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
|
||||
*/
|
||||
SmsResult send(String phones, String templateId, Map<String, String> param);
|
||||
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.ruoyi.common.utils.JsonUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.sms.exception.SmsException;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.sms.v20190711.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendStatus;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Tencent 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public class TencentSmsTemplate implements SmsTemplate {
|
||||
|
||||
private SmsProperties properties;
|
||||
|
||||
private SmsClient client;
|
||||
|
||||
@SneakyThrows(Exception.class)
|
||||
public TencentSmsTemplate(SmsProperties smsProperties) {
|
||||
this.properties = smsProperties;
|
||||
Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint(smsProperties.getEndpoint());
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
this.client = new SmsClient(credential, "", clientProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
||||
if (StringUtils.isBlank(phones)) {
|
||||
throw new SmsException("手机号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(templateId)) {
|
||||
throw new SmsException("模板ID不能为空");
|
||||
}
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
|
||||
req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
|
||||
if (CollUtil.isNotEmpty(param)) {
|
||||
req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
|
||||
}
|
||||
req.setTemplateID(templateId);
|
||||
req.setSign(properties.getSignName());
|
||||
req.setSmsSdkAppid(properties.getSdkAppId());
|
||||
try {
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
SmsResult.SmsResultBuilder builder = SmsResult.builder()
|
||||
.isSuccess(true)
|
||||
.message("send success")
|
||||
.response(JsonUtils.toJsonString(resp));
|
||||
for (SendStatus sendStatus : resp.getSendStatusSet()) {
|
||||
if (!"Ok".equals(sendStatus.getCode())) {
|
||||
builder.isSuccess(false).message(sendStatus.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
} catch (Exception e) {
|
||||
throw new SmsException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.ruoyi.sms.entity;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 上传返回体
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class SmsResult {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean isSuccess;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 实际响应体
|
||||
* <p>
|
||||
* 可自行转换为 SDK 对应的 SendSmsResponse
|
||||
*/
|
||||
private String response;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package com.ruoyi.sms.exception;
|
||||
|
||||
/**
|
||||
* Sms异常类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class SmsException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SmsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>ruoyi-vue-plus</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>4.7.0</version>
|
||||
<version>4.8.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
@ -2,12 +2,12 @@ package com.ruoyi.system.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.system.domain.SysOss;
|
||||
import com.ruoyi.system.domain.bo.SysOssBo;
|
||||
import com.ruoyi.system.domain.vo.SysOssVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -27,6 +27,8 @@ public interface ISysOssService {
|
||||
|
||||
SysOssVo upload(MultipartFile file);
|
||||
|
||||
SysOssVo upload(File file);
|
||||
|
||||
void download(Long ossId, HttpServletResponse response) throws IOException;
|
||||
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
@ -8,9 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.domain.event.LogininforEvent;
|
||||
import com.ruoyi.common.core.domain.dto.RoleDTO;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.event.LogininforEvent;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.domain.model.XcxLoginUser;
|
||||
import com.ruoyi.common.enums.DeviceType;
|
||||
@ -71,9 +71,10 @@ public class SysLoginService {
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, code, uuid);
|
||||
}
|
||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
||||
SysUser user = loadUserByUsername(username);
|
||||
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
|
||||
@ -88,7 +89,7 @@ public class SysLoginService {
|
||||
SysUser user = loadUserByPhonenumber(phonenumber);
|
||||
|
||||
checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
||||
@ -99,11 +100,11 @@ public class SysLoginService {
|
||||
}
|
||||
|
||||
public String emailLogin(String email, String emailCode) {
|
||||
// 通过手机号查找用户
|
||||
// 通过手邮箱查找用户
|
||||
SysUser user = loadUserByEmail(email);
|
||||
|
||||
checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
||||
@ -118,9 +119,11 @@ public class SysLoginService {
|
||||
// todo 以下自行实现
|
||||
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
||||
String openid = "";
|
||||
|
||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
||||
SysUser user = loadUserByOpenid(openid);
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
XcxLoginUser loginUser = new XcxLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
@ -301,25 +304,24 @@ public class SysLoginService {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
|
||||
Integer errorNumber = RedisUtils.getCacheObject(errorKey);
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 是否第一次
|
||||
errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber.equals(maxRetryCount)) {
|
||||
// 错误次数递增
|
||||
errorNumber++;
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||
// 达到规定错误次数 则锁定登录
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
} else {
|
||||
// 未达到规定错误次数 则递增
|
||||
RedisUtils.setCacheObject(errorKey, errorNumber);
|
||||
// 未达到规定错误次数
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||
}
|
||||
|
@ -116,7 +116,6 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
* @param dictType 字典类型
|
||||
* @return 字典类型
|
||||
*/
|
||||
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
||||
@Override
|
||||
public SysDictType selectDictTypeByType(String dictType) {
|
||||
return baseMapper.selectById(new LambdaQueryWrapper<SysDictType>().eq(SysDictType::getDictType, dictType));
|
||||
@ -148,7 +147,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
List<SysDictData> dictDataList = dictDataMapper.selectList(
|
||||
new LambdaQueryWrapper<SysDictData>().eq(SysDictData::getStatus, UserConstants.DICT_NORMAL));
|
||||
Map<String, List<SysDictData>> dictDataMap = StreamUtils.groupByKey(dictDataList, SysDictData::getDictType);
|
||||
dictDataMap.forEach((k,v) -> {
|
||||
dictDataMap.forEach((k, v) -> {
|
||||
List<SysDictData> dictList = StreamUtils.sorted(v, Comparator.comparing(SysDictData::getDictSort));
|
||||
CacheUtils.put(CacheNames.SYS_DICT, k, dictList);
|
||||
});
|
||||
@ -182,6 +181,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
public List<SysDictData> insertDictType(SysDictType dict) {
|
||||
int row = baseMapper.insert(dict);
|
||||
if (row > 0) {
|
||||
// 新增 type 下无 data 数据 返回空防止缓存穿透
|
||||
return new ArrayList<>();
|
||||
}
|
||||
throw new ServiceException("操作失败");
|
||||
@ -279,4 +279,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAllDictByDictType(String dictType) {
|
||||
List<SysDictData> list = selectDictDataByType(dictType);
|
||||
return StreamUtils.toMap(list, SysDictData::getDictValue, SysDictData::getDictLabel);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
@ -11,7 +12,6 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.service.OssService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.BeanCopyUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
@ -31,9 +31,13 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -108,7 +112,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
}
|
||||
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
|
||||
OssClient storage = OssFactory.instance();
|
||||
OssClient storage = OssFactory.instance(sysOss.getService());
|
||||
try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
|
||||
int available = inputStream.available();
|
||||
IoUtil.copy(inputStream, response.getOutputStream(), available);
|
||||
@ -130,15 +134,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
// 保存文件信息
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysOssVo upload(File file) {
|
||||
String originalfileName = file.getName();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance();
|
||||
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
|
||||
// 保存文件信息
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
|
||||
}
|
||||
|
||||
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
|
||||
SysOss oss = new SysOss();
|
||||
oss.setUrl(uploadResult.getUrl());
|
||||
oss.setFileSuffix(suffix);
|
||||
oss.setFileName(uploadResult.getFilename());
|
||||
oss.setOriginalName(originalfileName);
|
||||
oss.setService(storage.getConfigKey());
|
||||
oss.setService(configKey);
|
||||
baseMapper.insert(oss);
|
||||
SysOssVo sysOssVo = new SysOssVo();
|
||||
BeanCopyUtils.copy(oss, sysOssVo);
|
||||
SysOssVo sysOssVo = BeanUtil.toBean(oss, SysOssVo.class);
|
||||
return this.matchingUrl(sysOssVo);
|
||||
}
|
||||
|
||||
|
@ -186,6 +186,20 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
if (ObjectUtil.isNotNull(role.getRoleId()) && role.isAdmin()) {
|
||||
throw new ServiceException("不允许操作超级管理员角色");
|
||||
}
|
||||
// 新增不允许使用 管理员标识符
|
||||
if (ObjectUtil.isNull(role.getRoleId())
|
||||
&& StringUtils.equals(role.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
|
||||
throw new ServiceException("不允许使用系统内置管理员角色标识符!");
|
||||
}
|
||||
// 修改不允许修改 管理员标识符
|
||||
if (ObjectUtil.isNotNull(role.getRoleId())) {
|
||||
SysRole sysRole = baseMapper.selectById(role.getRoleId());
|
||||
// 如果标识符不相等 判断为修改了管理员标识符
|
||||
if (!StringUtils.equals(sysRole.getRoleKey(), role.getRoleKey())
|
||||
&& StringUtils.equals(sysRole.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
|
||||
throw new ServiceException("不允许修改系统内置管理员角色标识符!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,9 +356,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteRoleByIds(Long[] roleIds) {
|
||||
for (Long roleId : roleIds) {
|
||||
checkRoleAllowed(new SysRole(roleId));
|
||||
checkRoleDataScope(roleId);
|
||||
SysRole role = selectRoleById(roleId);
|
||||
checkRoleAllowed(role);
|
||||
checkRoleDataScope(roleId);
|
||||
if (countUserRoleByRoleId(roleId) > 0) {
|
||||
throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
|
||||
}
|
||||
@ -420,6 +434,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
|
||||
@Override
|
||||
public void cleanOnlineUserByRole(Long roleId) {
|
||||
// 如果角色未绑定用户 直接返回
|
||||
Long num = userRoleMapper.selectCount(new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, roleId));
|
||||
if (num == 0) {
|
||||
return;
|
||||
}
|
||||
List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
|
||||
if (CollUtil.isEmpty(keys)) {
|
||||
return;
|
||||
@ -428,11 +447,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
keys.parallelStream().forEach(key -> {
|
||||
String token = StringUtils.substringAfterLast(key, ":");
|
||||
// 如果已经过期则跳过
|
||||
if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
|
||||
if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
|
||||
return;
|
||||
}
|
||||
LoginUser loginUser = LoginHelper.getLoginUser(token);
|
||||
if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
|
||||
if (ObjectUtil.isNotNull(loginUser) && loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
|
||||
try {
|
||||
StpUtil.logoutByTokenValue(token);
|
||||
} catch (NotLoginException ignored) {
|
||||
|
21
ruoyi-ui-vue3/.editorconfig
Normal file
@ -0,0 +1,21 @@
|
||||
# 告诉EditorConfig插件,这是根文件,不用继续往上查找
|
||||
root = true
|
||||
|
||||
# 匹配全部文件
|
||||
[*]
|
||||
# 缩进风格,可选space、tab
|
||||
indent_style = space
|
||||
# 缩进的空格数
|
||||
indent_size = 2
|
||||
# 设置字符集
|
||||
charset = utf-8
|
||||
# 结尾换行符,可选lf、cr、crlf
|
||||
end_of_line = lf
|
||||
# 在文件结尾插入新行
|
||||
trim_trailing_whitespace = true
|
||||
# 删除一行中的前后空格
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
17
ruoyi-ui-vue3/.env.development
Normal file
@ -0,0 +1,17 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
# 若依管理系统/开发环境
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
# 应用访问路径 例如使用前缀 /admin/
|
||||
VITE_APP_CONTEXT_PATH = '/'
|
||||
|
||||
# 监控地址
|
||||
VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
|
||||
|
||||
# xxl-job 控制台地址
|
||||
VITE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'
|
20
ruoyi-ui-vue3/.env.production
Normal file
@ -0,0 +1,20 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
# 应用访问路径 例如使用前缀 /admin/
|
||||
VITE_APP_CONTEXT_PATH = '/'
|
||||
|
||||
# 监控地址
|
||||
VITE_APP_MONITRO_ADMIN = '/admin/applications'
|
||||
|
||||
# 监控地址
|
||||
VITE_APP_XXL_JOB_ADMIN = '/xxl-job-admin'
|
||||
|
||||
# 若依管理系统/生产环境
|
||||
VITE_APP_BASE_API = '/prod-api'
|
||||
|
||||
# 是否在打包时开启压缩,支持 gzip 和 brotli
|
||||
VITE_BUILD_COMPRESS = gzip
|
23
ruoyi-ui-vue3/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
**/*.log
|
||||
|
||||
tests/**/coverage/
|
||||
tests/e2e/reports
|
||||
selenium-debug.log
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.local
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
83
ruoyi-ui-vue3/README.md
Normal file
@ -0,0 +1,83 @@
|
||||
## 平台简介
|
||||
|
||||
* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
|
||||
* 配套后端代码仓库地址[RuoYi-Vue-Plus 4.X(注意版本号)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus)
|
||||
* 5.X后端需要使用此项目 [plus-ui](https://gitee.com/JavaLionLi/plus-ui)
|
||||
|
||||
## 前端运行
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd ruoyi-ui-vue3
|
||||
|
||||
# 安装依赖
|
||||
npm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
|
||||
# 构建测试环境 yarn build:stage
|
||||
# 构建生产环境 yarn build:prod
|
||||
# 前端访问地址 http://localhost:80
|
||||
```
|
||||
|
||||
## 后端改造
|
||||
参考后端代码内 `ruoyi-generator/resources/vm/vue/v3/readme.txt` 说明
|
||||
|
||||
## 内置功能
|
||||
|
||||
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
|
||||
3. 岗位管理:配置系统用户所属担任职务。
|
||||
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
|
||||
6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||
7. 参数管理:对系统动态配置常用参数。
|
||||
8. 通知公告:系统通知公告信息发布维护。
|
||||
9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
10. 登录日志:系统登录日志记录查询包含登录异常。
|
||||
11. 在线用户:当前系统中活跃用户状态监控。
|
||||
12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
|
||||
13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
|
||||
14. 系统接口:根据业务代码自动生成相关的api接口文档。
|
||||
15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
|
||||
16. 缓存监控:对系统的缓存信息查询,命令统计等。
|
||||
17. 在线构建器:拖动表单元素生成相应的HTML代码。
|
||||
18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
|
||||
|
||||
## 演示图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/cd1f90be5f2684f4560c9519c0f2a232ee8.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/1cbcf0e6f257c7d3a063c0e3f2ff989e4b3.jpg"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-8074972883b5ba0622e13246738ebba237a.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-9f88719cdfca9af2e58b352a20e23d43b12.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-39bf2584ec3a529b0d5a3b70d15c9b37646.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-936ec82d1f4872e1bc980927654b6007307.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-b2d62ceb95d2dd9b3fbe157bb70d26001e9.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-d67451d308b7a79ad6819723396f7c3d77a.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/5e8c387724954459291aafd5eb52b456f53.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/644e78da53c2e92a95dfda4f76e6d117c4b.jpg"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-8370a0d02977eebf6dbf854c8450293c937.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-49003ed83f60f633e7153609a53a2b644f7.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-d4fe726319ece268d4746602c39cffc0621.png"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-c195234bbcd30be6927f037a6755e6ab69c.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
|
||||
<td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
|
||||
</tr>
|
||||
</table>
|
12
ruoyi-ui-vue3/bin/build.bat
Normal file
@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo [信息] 打包Web工程,生成dist文件。
|
||||
echo.
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
yarn build:prod
|
||||
|
||||
pause
|
12
ruoyi-ui-vue3/bin/package.bat
Normal file
@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo [信息] 安装Web工程,生成node_modules文件。
|
||||
echo.
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
yarn --registry=https://registry.npmmirror.com
|
||||
|
||||
pause
|
12
ruoyi-ui-vue3/bin/run-web.bat
Normal file
@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo [信息] 使用 Vite 命令运行 Web 工程。
|
||||
echo.
|
||||
|
||||
%~d0
|
||||
cd %~dp0
|
||||
|
||||
cd ..
|
||||
yarn dev
|
||||
|
||||
pause
|
46
ruoyi-ui-vue3/html/ie.html
Normal file
215
ruoyi-ui-vue3/index.html
Normal file
@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>RuoYi-Vue-Plus管理系统</title>
|
||||
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.chromeframe {
|
||||
margin: 0.2em 0;
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#loader {
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: -75px 0 0 -75px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
-ms-animation: spin 2s linear infinite;
|
||||
-moz-animation: spin 2s linear infinite;
|
||||
-o-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#loader:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-webkit-animation: spin 3s linear infinite;
|
||||
-moz-animation: spin 3s linear infinite;
|
||||
-o-animation: spin 3s linear infinite;
|
||||
-ms-animation: spin 3s linear infinite;
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
#loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #FFF;
|
||||
-moz-animation: spin 1.5s linear infinite;
|
||||
-o-animation: spin 1.5s linear infinite;
|
||||
-ms-animation: spin 1.5s linear infinite;
|
||||
-webkit-animation: spin 1.5s linear infinite;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#loader-wrapper .loader-section {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 51%;
|
||||
height: 100%;
|
||||
background: #7171C6;
|
||||
z-index: 1000;
|
||||
-webkit-transform: translateX(0);
|
||||
-ms-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#loader-wrapper .loader-section.section-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-left {
|
||||
-webkit-transform: translateX(-100%);
|
||||
-ms-transform: translateX(-100%);
|
||||
transform: translateX(-100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper .loader-section.section-right {
|
||||
-webkit-transform: translateX(100%);
|
||||
-ms-transform: translateX(100%);
|
||||
transform: translateX(100%);
|
||||
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
|
||||
}
|
||||
|
||||
.loaded #loader {
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.loaded #loader-wrapper {
|
||||
visibility: hidden;
|
||||
-webkit-transform: translateY(-100%);
|
||||
-ms-transform: translateY(-100%);
|
||||
transform: translateY(-100%);
|
||||
-webkit-transition: all 0.3s 1s ease-out;
|
||||
transition: all 0.3s 1s ease-out;
|
||||
}
|
||||
|
||||
.no-js #loader-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-js h1 {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title {
|
||||
font-family: 'Open Sans';
|
||||
color: #FFF;
|
||||
font-size: 19px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
z-index: 9999999999999;
|
||||
position: absolute;
|
||||
top: 60%;
|
||||
opacity: 1;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
#loader-wrapper .load_title span {
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="loader-wrapper">
|
||||
<div id="loader"></div>
|
||||
<div class="loader-section section-left"></div>
|
||||
<div class="loader-section section-right"></div>
|
||||
<div class="load_title">正在加载系统资源,请耐心等待</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
43
ruoyi-ui-vue3/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "ruoyi-vue-plus",
|
||||
"version": "4.8.0",
|
||||
"description": "RuoYi-Vue-Plus后台管理系统",
|
||||
"author": "LionLi",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:prod": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.0.10",
|
||||
"@vueup/vue-quill": "1.1.0",
|
||||
"@vueuse/core": "9.5.0",
|
||||
"axios": "0.27.2",
|
||||
"echarts": "5.4.0",
|
||||
"element-plus": "2.2.27",
|
||||
"file-saver": "2.0.5",
|
||||
"fuse.js": "6.6.2",
|
||||
"js-cookie": "3.0.1",
|
||||
"jsencrypt": "3.3.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.0.22",
|
||||
"vue": "3.2.45",
|
||||
"vue-cropper": "1.0.3",
|
||||
"vue-router": "4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "3.1.0",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"sass": "1.56.1",
|
||||
"unplugin-auto-import": "0.11.4",
|
||||
"vite": "3.2.3",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-setup-extend": "0.4.0"
|
||||
}
|
||||
}
|
BIN
ruoyi-ui-vue3/public/favicon.ico
Normal file
After Width: | Height: | Size: 7.9 KiB |
15
ruoyi-ui-vue3/src/App.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { handleThemeStyle } from '@/utils/theme'
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 初始化主题样式
|
||||
handleThemeStyle(useSettingsStore().theme)
|
||||
})
|
||||
})
|
||||
</script>
|
54
ruoyi-ui-vue3/src/api/demo/demo.js
Normal file
@ -0,0 +1,54 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询测试单表列表
|
||||
export function listDemo(query) {
|
||||
return request({
|
||||
url: '/demo/demo/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 自定义分页接口
|
||||
export function pageDemo(query) {
|
||||
return request({
|
||||
url: '/demo/demo/page',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询测试单表详细
|
||||
export function getDemo(id) {
|
||||
return request({
|
||||
url: '/demo/demo/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增测试单表
|
||||
export function addDemo(data) {
|
||||
return request({
|
||||
url: '/demo/demo',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改测试单表
|
||||
export function updateDemo(data) {
|
||||
return request({
|
||||
url: '/demo/demo',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除测试单表
|
||||
export function delDemo(id) {
|
||||
return request({
|
||||
url: '/demo/demo/' + id,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
44
ruoyi-ui-vue3/src/api/demo/tree.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询测试树表列表
|
||||
export function listTree(query) {
|
||||
return request({
|
||||
url: '/demo/tree/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询测试树表详细
|
||||
export function getTree(id) {
|
||||
return request({
|
||||
url: '/demo/tree/' + id,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增测试树表
|
||||
export function addTree(data) {
|
||||
return request({
|
||||
url: '/demo/tree',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改测试树表
|
||||
export function updateTree(data) {
|
||||
return request({
|
||||
url: '/demo/tree',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除测试树表
|
||||
export function delTree(id) {
|
||||
return request({
|
||||
url: '/demo/tree/' + id,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
59
ruoyi-ui-vue3/src/api/login.js
Normal file
@ -0,0 +1,59 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 登录方法
|
||||
export function login(username, password, code, uuid) {
|
||||
const data = {
|
||||
username,
|
||||
password,
|
||||
code,
|
||||
uuid
|
||||
}
|
||||
return request({
|
||||
url: '/login',
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 注册方法
|
||||
export function register(data) {
|
||||
return request({
|
||||
url: '/register',
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
export function getInfo() {
|
||||
return request({
|
||||
url: '/getInfo',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 退出方法
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取验证码
|
||||
export function getCodeImg() {
|
||||
return request({
|
||||
url: '/captchaImage',
|
||||
headers: {
|
||||
isToken: false
|
||||
},
|
||||
method: 'get',
|
||||
timeout: 20000
|
||||
})
|
||||
}
|
9
ruoyi-ui-vue3/src/api/menu.js
Normal file
@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取路由
|
||||
export const getRouters = () => {
|
||||
return request({
|
||||
url: '/getRouters',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
57
ruoyi-ui-vue3/src/api/monitor/cache.js
Normal file
@ -0,0 +1,57 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询缓存详细
|
||||
export function getCache() {
|
||||
return request({
|
||||
url: '/monitor/cache',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询缓存名称列表
|
||||
export function listCacheName() {
|
||||
return request({
|
||||
url: '/monitor/cache/getNames',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询缓存键名列表
|
||||
export function listCacheKey(cacheName) {
|
||||
return request({
|
||||
url: '/monitor/cache/getKeys/' + cacheName,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询缓存内容
|
||||
export function getCacheValue(cacheName, cacheKey) {
|
||||
return request({
|
||||
url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 清理指定名称缓存
|
||||
export function clearCacheName(cacheName) {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheName/' + cacheName,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 清理指定键名缓存
|
||||
export function clearCacheKey(cacheName, cacheKey) {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 清理全部缓存
|
||||
export function clearCacheAll() {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheAll',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
34
ruoyi-ui-vue3/src/api/monitor/logininfor.js
Normal file
@ -0,0 +1,34 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询登录日志列表
|
||||
export function list(query) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 删除登录日志
|
||||
export function delLogininfor(infoId) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/' + infoId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 解锁用户登录状态
|
||||
export function unlockLogininfor(userName) {
|
||||
return request({
|
||||
url: '/monitor/logininfor/unlock/' + userName,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 清空登录日志
|
||||
export function cleanLogininfor() {
|
||||
return request({
|
||||
url: '/monitor/logininfor/clean',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
18
ruoyi-ui-vue3/src/api/monitor/online.js
Normal file
@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询在线用户列表
|
||||
export function list(query) {
|
||||
return request({
|
||||
url: '/monitor/online/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 强退用户
|
||||
export function forceLogout(tokenId) {
|
||||
return request({
|
||||
url: '/monitor/online/' + tokenId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
26
ruoyi-ui-vue3/src/api/monitor/operlog.js
Normal file
@ -0,0 +1,26 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询操作日志列表
|
||||
export function list(query) {
|
||||
return request({
|
||||
url: '/monitor/operlog/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 删除操作日志
|
||||
export function delOperlog(operId) {
|
||||
return request({
|
||||
url: '/monitor/operlog/' + operId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 清空操作日志
|
||||
export function cleanOperlog() {
|
||||
return request({
|
||||
url: '/monitor/operlog/clean',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
72
ruoyi-ui-vue3/src/api/system/config.js
Normal file
@ -0,0 +1,72 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询参数列表
|
||||
export function listConfig(query) {
|
||||
return request({
|
||||
url: '/system/config/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询参数详细
|
||||
export function getConfig(configId) {
|
||||
return request({
|
||||
url: '/system/config/' + configId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据参数键名查询参数值
|
||||
export function getConfigKey(configKey) {
|
||||
return request({
|
||||
url: '/system/config/configKey/' + configKey,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增参数配置
|
||||
export function addConfig(data) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改参数配置
|
||||
export function updateConfig(data) {
|
||||
return request({
|
||||
url: '/system/config',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改参数配置
|
||||
export function updateConfigByKey(key, value) {
|
||||
return request({
|
||||
url: '/system/config/updateByKey',
|
||||
method: 'put',
|
||||
data: {
|
||||
configKey: key,
|
||||
configValue: value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除参数配置
|
||||
export function delConfig(configId) {
|
||||
return request({
|
||||
url: '/system/config/' + configId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新参数缓存
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/config/refreshCache',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
52
ruoyi-ui-vue3/src/api/system/dept.js
Normal file
@ -0,0 +1,52 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询部门列表
|
||||
export function listDept(query) {
|
||||
return request({
|
||||
url: '/system/dept/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询部门列表(排除节点)
|
||||
export function listDeptExcludeChild(deptId) {
|
||||
return request({
|
||||
url: '/system/dept/list/exclude/' + deptId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询部门详细
|
||||
export function getDept(deptId) {
|
||||
return request({
|
||||
url: '/system/dept/' + deptId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增部门
|
||||
export function addDept(data) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改部门
|
||||
export function updateDept(data) {
|
||||
return request({
|
||||
url: '/system/dept',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除部门
|
||||
export function delDept(deptId) {
|
||||
return request({
|
||||
url: '/system/dept/' + deptId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
52
ruoyi-ui-vue3/src/api/system/dict/data.js
Normal file
@ -0,0 +1,52 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询字典数据列表
|
||||
export function listData(query) {
|
||||
return request({
|
||||
url: '/system/dict/data/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询字典数据详细
|
||||
export function getData(dictCode) {
|
||||
return request({
|
||||
url: '/system/dict/data/' + dictCode,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据字典类型查询字典数据信息
|
||||
export function getDicts(dictType) {
|
||||
return request({
|
||||
url: '/system/dict/data/type/' + dictType,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增字典数据
|
||||
export function addData(data) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改字典数据
|
||||
export function updateData(data) {
|
||||
return request({
|
||||
url: '/system/dict/data',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典数据
|
||||
export function delData(dictCode) {
|
||||
return request({
|
||||
url: '/system/dict/data/' + dictCode,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
60
ruoyi-ui-vue3/src/api/system/dict/type.js
Normal file
@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询字典类型列表
|
||||
export function listType(query) {
|
||||
return request({
|
||||
url: '/system/dict/type/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询字典类型详细
|
||||
export function getType(dictId) {
|
||||
return request({
|
||||
url: '/system/dict/type/' + dictId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增字典类型
|
||||
export function addType(data) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改字典类型
|
||||
export function updateType(data) {
|
||||
return request({
|
||||
url: '/system/dict/type',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
export function delType(dictId) {
|
||||
return request({
|
||||
url: '/system/dict/type/' + dictId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新字典缓存
|
||||
export function refreshCache() {
|
||||
return request({
|
||||
url: '/system/dict/type/refreshCache',
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取字典选择框列表
|
||||
export function optionselect() {
|
||||
return request({
|
||||
url: '/system/dict/type/optionselect',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
60
ruoyi-ui-vue3/src/api/system/menu.js
Normal file
@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询菜单列表
|
||||
export function listMenu(query) {
|
||||
return request({
|
||||
url: '/system/menu/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询菜单详细
|
||||
export function getMenu(menuId) {
|
||||
return request({
|
||||
url: '/system/menu/' + menuId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询菜单下拉树结构
|
||||
export function treeselect() {
|
||||
return request({
|
||||
url: '/system/menu/treeselect',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 根据角色ID查询菜单下拉树结构
|
||||
export function roleMenuTreeselect(roleId) {
|
||||
return request({
|
||||
url: '/system/menu/roleMenuTreeselect/' + roleId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增菜单
|
||||
export function addMenu(data) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改菜单
|
||||
export function updateMenu(data) {
|
||||
return request({
|
||||
url: '/system/menu',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
export function delMenu(menuId) {
|
||||
return request({
|
||||
url: '/system/menu/' + menuId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
44
ruoyi-ui-vue3/src/api/system/notice.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询公告列表
|
||||
export function listNotice(query) {
|
||||
return request({
|
||||
url: '/system/notice/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询公告详细
|
||||
export function getNotice(noticeId) {
|
||||
return request({
|
||||
url: '/system/notice/' + noticeId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增公告
|
||||
export function addNotice(data) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改公告
|
||||
export function updateNotice(data) {
|
||||
return request({
|
||||
url: '/system/notice',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除公告
|
||||
export function delNotice(noticeId) {
|
||||
return request({
|
||||
url: '/system/notice/' + noticeId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
27
ruoyi-ui-vue3/src/api/system/oss.js
Normal file
@ -0,0 +1,27 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询OSS对象存储列表
|
||||
export function listOss(query) {
|
||||
return request({
|
||||
url: '/system/oss/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询OSS对象基于id串
|
||||
export function listByIds(ossId) {
|
||||
return request({
|
||||
url: '/system/oss/listByIds/' + ossId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除OSS对象存储
|
||||
export function delOss(ossId) {
|
||||
return request({
|
||||
url: '/system/oss/' + ossId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
58
ruoyi-ui-vue3/src/api/system/ossConfig.js
Normal file
@ -0,0 +1,58 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询对象存储配置列表
|
||||
export function listOssConfig(query) {
|
||||
return request({
|
||||
url: '/system/oss/config/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询对象存储配置详细
|
||||
export function getOssConfig(ossConfigId) {
|
||||
return request({
|
||||
url: '/system/oss/config/' + ossConfigId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增对象存储配置
|
||||
export function addOssConfig(data) {
|
||||
return request({
|
||||
url: '/system/oss/config',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改对象存储配置
|
||||
export function updateOssConfig(data) {
|
||||
return request({
|
||||
url: '/system/oss/config',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除对象存储配置
|
||||
export function delOssConfig(ossConfigId) {
|
||||
return request({
|
||||
url: '/system/oss/config/' + ossConfigId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 对象存储状态修改
|
||||
export function changeOssConfigStatus(ossConfigId, status, configKey) {
|
||||
const data = {
|
||||
ossConfigId,
|
||||
status,
|
||||
configKey
|
||||
}
|
||||
return request({
|
||||
url: '/system/oss/config/changeStatus',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
44
ruoyi-ui-vue3/src/api/system/post.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询岗位列表
|
||||
export function listPost(query) {
|
||||
return request({
|
||||
url: '/system/post/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询岗位详细
|
||||
export function getPost(postId) {
|
||||
return request({
|
||||
url: '/system/post/' + postId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增岗位
|
||||
export function addPost(data) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改岗位
|
||||
export function updatePost(data) {
|
||||
return request({
|
||||
url: '/system/post',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除岗位
|
||||
export function delPost(postId) {
|
||||
return request({
|
||||
url: '/system/post/' + postId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
119
ruoyi-ui-vue3/src/api/system/role.js
Normal file
@ -0,0 +1,119 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询角色列表
|
||||
export function listRole(query) {
|
||||
return request({
|
||||
url: '/system/role/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询角色详细
|
||||
export function getRole(roleId) {
|
||||
return request({
|
||||
url: '/system/role/' + roleId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增角色
|
||||
export function addRole(data) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改角色
|
||||
export function updateRole(data) {
|
||||
return request({
|
||||
url: '/system/role',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 角色数据权限
|
||||
export function dataScope(data) {
|
||||
return request({
|
||||
url: '/system/role/dataScope',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 角色状态修改
|
||||
export function changeRoleStatus(roleId, status) {
|
||||
const data = {
|
||||
roleId,
|
||||
status
|
||||
}
|
||||
return request({
|
||||
url: '/system/role/changeStatus',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
export function delRole(roleId) {
|
||||
return request({
|
||||
url: '/system/role/' + roleId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 查询角色已授权用户列表
|
||||
export function allocatedUserList(query) {
|
||||
return request({
|
||||
url: '/system/role/authUser/allocatedList',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询角色未授权用户列表
|
||||
export function unallocatedUserList(query) {
|
||||
return request({
|
||||
url: '/system/role/authUser/unallocatedList',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 取消用户授权角色
|
||||
export function authUserCancel(data) {
|
||||
return request({
|
||||
url: '/system/role/authUser/cancel',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 批量取消用户授权角色
|
||||
export function authUserCancelAll(data) {
|
||||
return request({
|
||||
url: '/system/role/authUser/cancelAll',
|
||||
method: 'put',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
// 授权用户选择
|
||||
export function authUserSelectAll(data) {
|
||||
return request({
|
||||
url: '/system/role/authUser/selectAll',
|
||||
method: 'put',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
// 根据角色ID查询部门树结构
|
||||
export function deptTreeSelect(roleId) {
|
||||
return request({
|
||||
url: '/system/role/deptTree/' + roleId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
135
ruoyi-ui-vue3/src/api/system/user.js
Normal file
@ -0,0 +1,135 @@
|
||||
import request from '@/utils/request'
|
||||
import { parseStrEmpty } from "@/utils/ruoyi";
|
||||
|
||||
// 查询用户列表
|
||||
export function listUser(query) {
|
||||
return request({
|
||||
url: '/system/user/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询用户详细
|
||||
export function getUser(userId) {
|
||||
return request({
|
||||
url: '/system/user/' + parseStrEmpty(userId),
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改用户
|
||||
export function updateUser(data) {
|
||||
return request({
|
||||
url: '/system/user',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export function delUser(userId) {
|
||||
return request({
|
||||
url: '/system/user/' + userId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 用户密码重置
|
||||
export function resetUserPwd(userId, password) {
|
||||
const data = {
|
||||
userId,
|
||||
password
|
||||
}
|
||||
return request({
|
||||
url: '/system/user/resetPwd',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 用户状态修改
|
||||
export function changeUserStatus(userId, status) {
|
||||
const data = {
|
||||
userId,
|
||||
status
|
||||
}
|
||||
return request({
|
||||
url: '/system/user/changeStatus',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询用户个人信息
|
||||
export function getUserProfile() {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 修改用户个人信息
|
||||
export function updateUserProfile(data) {
|
||||
return request({
|
||||
url: '/system/user/profile',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 用户密码重置
|
||||
export function updateUserPwd(oldPassword, newPassword) {
|
||||
const data = {
|
||||
oldPassword,
|
||||
newPassword
|
||||
}
|
||||
return request({
|
||||
url: '/system/user/profile/updatePwd',
|
||||
method: 'put',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
// 用户头像上传
|
||||
export function uploadAvatar(data) {
|
||||
return request({
|
||||
url: '/system/user/profile/avatar',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询授权角色
|
||||
export function getAuthRole(userId) {
|
||||
return request({
|
||||
url: '/system/user/authRole/' + userId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 保存授权角色
|
||||
export function updateAuthRole(data) {
|
||||
return request({
|
||||
url: '/system/user/authRole',
|
||||
method: 'put',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
// 查询部门下拉树结构
|
||||
export function deptTreeSelect() {
|
||||
return request({
|
||||
url: '/system/user/deptTree',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
85
ruoyi-ui-vue3/src/api/tool/gen.js
Normal file
@ -0,0 +1,85 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询生成表数据
|
||||
export function listTable(query) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
// 查询db数据库列表
|
||||
export function listDbTable(query) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/db/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询表详细信息
|
||||
export function getGenTable(tableId) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/' + tableId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 修改代码生成信息
|
||||
export function updateGenTable(data) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 导入表
|
||||
export function importTable(data) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/importTable',
|
||||
method: 'post',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
// 预览生成代码
|
||||
export function previewTable(tableId) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/preview/' + tableId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除表数据
|
||||
export function delTable(tableId) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/' + tableId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 生成代码(自定义路径)
|
||||
export function genCode(tableName) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/genCode/' + tableName,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 同步数据库
|
||||
export function synchDb(tableName) {
|
||||
return request({
|
||||
headers: { 'datasource': localStorage.getItem("dataName") },
|
||||
url: '/tool/gen/synchDb/' + tableName,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
BIN
ruoyi-ui-vue3/src/assets/401_images/401.gif
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
ruoyi-ui-vue3/src/assets/404_images/404.png
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
ruoyi-ui-vue3/src/assets/404_images/404_cloud.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
1
ruoyi-ui-vue3/src/assets/icons/svg/404.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
ruoyi-ui-vue3/src/assets/icons/svg/bug.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
1
ruoyi-ui-vue3/src/assets/icons/svg/build.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899741379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M960 591.424V368.96c0-0.288 0.16-0.512 0.16-0.768S960 367.68 960 367.424V192a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v175.424c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768v222.464c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768V864a32 32 0 0 0 32 32h832a32 32 0 0 0 32-32v-271.04c0-0.288 0.16-0.512 0.16-0.768S960 591.68 960 591.424z m-560-31.232v-160H608v160h-208z m208 64V832h-208v-207.808H608z m-480-224h208v160H128v-160z m544 0h224v160h-224v-160zM896 224v112.192H128V224h768zM128 624.192h208V832H128v-207.808zM672 832v-207.808h224V832h-224z" p-id="2055"></path></svg>
|
After Width: | Height: | Size: 954 B |
1
ruoyi-ui-vue3/src/assets/icons/svg/button.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1588670460195" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1314" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M230.4 307.712c13.824 0 25.088-11.264 25.088-25.088 0-100.352 81.92-182.272 182.272-182.272s182.272 81.408 182.272 182.272c0 13.824 11.264 25.088 25.088 25.088s25.088-11.264 24.576-25.088c0-127.488-103.936-231.936-231.936-231.936S205.824 154.624 205.824 282.624c-0.512 14.336 10.752 25.088 24.576 25.088z m564.736 234.496c-11.264 0-21.504 2.048-31.232 6.144 0-44.544-40.448-81.92-88.064-81.92-14.848 0-28.16 3.584-39.936 10.24-13.824-28.16-44.544-48.128-78.848-48.128-12.288 0-24.576 2.56-35.328 7.68V284.16c0-45.568-37.888-81.92-84.48-81.92s-84.48 36.864-84.48 81.92v348.672l-69.12-112.64c-18.432-28.16-58.368-36.864-91.136-19.968-26.624 14.336-46.592 47.104-30.208 88.064 3.072 8.192 76.8 205.312 171.52 311.296 0 0 28.16 24.576 43.008 58.88 4.096 9.728 13.312 15.36 22.528 15.36 3.072 0 6.656-0.512 9.728-2.048 12.288-5.12 18.432-19.968 12.8-32.256-19.456-44.544-53.76-74.752-53.76-74.752C281.6 768 209.408 573.44 208.384 570.88c-5.12-12.8-2.56-20.992 7.168-26.112 9.216-4.608 21.504-4.608 26.112 2.56l113.152 184.32c4.096 8.704 12.8 14.336 22.528 14.336 13.824 0 25.088-10.752 25.088-25.088V284.16c0-17.92 15.36-32.256 34.816-32.256s34.816 14.336 34.816 32.256v284.16c0 13.824 10.24 25.088 24.576 25.088 13.824 0 25.088-11.264 25.088-25.088v-57.344c0-17.92 15.36-32.768 34.816-32.768 19.968 0 37.376 15.36 37.376 32.768v95.232c0 7.168 3.072 13.312 7.68 17.92 4.608 4.608 10.752 7.168 17.92 7.168 13.824 0 24.576-11.264 24.576-25.088V547.84c0-18.432 13.824-32.256 32.256-32.256 20.48 0 38.912 15.36 38.912 32.256v95.232c0 13.824 11.264 25.088 25.088 25.088s24.576-11.264 25.088-25.088v-18.944c0-18.944 12.8-32.256 30.72-32.256 18.432 0 22.528 18.944 22.528 31.744 0 1.024-11.776 99.84-50.688 173.056-30.72 58.368-45.056 112.128-51.2 146.944-2.56 13.312 6.656 26.112 19.968 28.672 1.536 0 3.072 0.512 4.608 0.512 11.776 0 22.016-8.192 24.064-20.48 5.632-31.232 18.432-79.36 46.08-132.608 43.52-81.92 55.808-186.88 56.32-193.536-0.512-50.688-29.696-83.968-72.704-83.968z"></path></path></svg>
|
After Width: | Height: | Size: 2.3 KiB |