(Arrays.asList(info.getAlarmEmail().split(",")));
- for (String email: emailSet) {
+ for (String email : emailSet) {
// make mail
try {
@@ -88,28 +89,28 @@ public class EmailJobAlarm implements JobAlarm {
*
* @return
*/
- private static final String loadEmailJobAlarmTemplate(){
+ private static final String loadEmailJobAlarmTemplate() {
String mailBodyTemplate = "" + I18nUtil.getString("jobconf_monitor_detail") + ":" +
- " \n" +
- " " +
- " \n" +
- " "+ I18nUtil.getString("jobinfo_field_jobgroup") +" \n" +
- " "+ I18nUtil.getString("jobinfo_field_id") +" \n" +
- " "+ I18nUtil.getString("jobinfo_field_jobdesc") +" \n" +
- " "+ I18nUtil.getString("jobconf_monitor_alarm_title") +" \n" +
- " "+ I18nUtil.getString("jobconf_monitor_alarm_content") +" \n" +
- " \n" +
- " \n" +
- " \n" +
- " \n" +
- " {0} \n" +
- " {1} \n" +
- " {2} \n" +
- " "+ I18nUtil.getString("jobconf_monitor_alarm_type") +" \n" +
- " {3} \n" +
- " \n" +
- " \n" +
- "
";
+ "\n" +
+ " " +
+ " \n" +
+ " " + I18nUtil.getString("jobinfo_field_jobgroup") + " \n" +
+ " " + I18nUtil.getString("jobinfo_field_id") + " \n" +
+ " " + I18nUtil.getString("jobinfo_field_jobdesc") + " \n" +
+ " " + I18nUtil.getString("jobconf_monitor_alarm_title") + " \n" +
+ " " + I18nUtil.getString("jobconf_monitor_alarm_content") + " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " {0} \n" +
+ " {1} \n" +
+ " {2} \n" +
+ " " + I18nUtil.getString("jobconf_monitor_alarm_type") + " \n" +
+ " {3} \n" +
+ " \n" +
+ " \n" +
+ "
";
return mailBodyTemplate;
}
diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java
index b9ac59a38..83399336b 100644
--- a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java
+++ b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/complete/XxlJobCompleter.java
@@ -32,7 +32,7 @@ public class XxlJobCompleter {
// text最大64kb 避免长度过长
if (xxlJobLog.getHandleMsg().length() > 15000) {
- xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg().substring(0, 15000) );
+ xxlJobLog.setHandleMsg(xxlJobLog.getHandleMsg().substring(0, 15000));
}
// fresh handle
@@ -43,18 +43,18 @@ public class XxlJobCompleter {
/**
* do somethind to finish job
*/
- private static void finishJob(XxlJobLog xxlJobLog){
+ private static void finishJob(XxlJobLog xxlJobLog) {
// 1、handle success, to trigger child job
String triggerChildMsg = null;
- if (XxlJobContext.HANDLE_COCE_SUCCESS == xxlJobLog.getHandleCode()) {
+ if (XxlJobContext.HANDLE_CODE_SUCCESS == xxlJobLog.getHandleCode()) {
XxlJobInfo xxlJobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(xxlJobLog.getJobId());
- if (xxlJobInfo!=null && xxlJobInfo.getChildJobId()!=null && xxlJobInfo.getChildJobId().trim().length()>0) {
- triggerChildMsg = " >>>>>>>>>>>"+ I18nUtil.getString("jobconf_trigger_child_run") +"<<<<<<<<<<< ";
+ if (xxlJobInfo != null && xxlJobInfo.getChildJobId() != null && xxlJobInfo.getChildJobId().trim().length() > 0) {
+ triggerChildMsg = " >>>>>>>>>>>" + I18nUtil.getString("jobconf_trigger_child_run") + "<<<<<<<<<<< ";
String[] childJobIds = xxlJobInfo.getChildJobId().split(",");
for (int i = 0; i < childJobIds.length; i++) {
- int childJobId = (childJobIds[i]!=null && childJobIds[i].trim().length()>0 && isNumeric(childJobIds[i]))?Integer.valueOf(childJobIds[i]):-1;
+ int childJobId = (childJobIds[i] != null && childJobIds[i].trim().length() > 0 && isNumeric(childJobIds[i])) ? Integer.valueOf(childJobIds[i]) : -1;
if (childJobId > 0) {
JobTriggerPoolHelper.trigger(childJobId, TriggerTypeEnum.PARENT, -1, null, null, null);
@@ -62,16 +62,16 @@ public class XxlJobCompleter {
// add msg
triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg1"),
- (i+1),
- childJobIds.length,
- childJobIds[i],
- (triggerChildResult.getCode()==ReturnT.SUCCESS_CODE?I18nUtil.getString("system_success"):I18nUtil.getString("system_fail")),
- triggerChildResult.getMsg());
+ (i + 1),
+ childJobIds.length,
+ childJobIds[i],
+ (triggerChildResult.getCode() == ReturnT.SUCCESS_CODE ? I18nUtil.getString("system_success") : I18nUtil.getString("system_fail")),
+ triggerChildResult.getMsg());
} else {
triggerChildMsg += MessageFormat.format(I18nUtil.getString("jobconf_callback_child_msg2"),
- (i+1),
- childJobIds.length,
- childJobIds[i]);
+ (i + 1),
+ childJobIds.length,
+ childJobIds[i]);
}
}
@@ -79,7 +79,7 @@ public class XxlJobCompleter {
}
if (triggerChildMsg != null) {
- xxlJobLog.setHandleMsg( xxlJobLog.getHandleMsg() + triggerChildMsg );
+ xxlJobLog.setHandleMsg(xxlJobLog.getHandleMsg() + triggerChildMsg);
}
// 2、fix_delay trigger next
@@ -87,7 +87,7 @@ public class XxlJobCompleter {
}
- private static boolean isNumeric(String str){
+ private static boolean isNumeric(String str) {
try {
int result = Integer.valueOf(str);
return true;
diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java
index 380b8a596..6e40cb760 100644
--- a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java
+++ b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/conf/XxlJobAdminConfig.java
@@ -23,6 +23,7 @@ import java.util.Arrays;
public class XxlJobAdminConfig implements InitializingBean, DisposableBean {
private static XxlJobAdminConfig adminConfig = null;
+
public static XxlJobAdminConfig getAdminConfig() {
return adminConfig;
}
diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
index fce23524d..2ce373eeb 100644
--- a/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
+++ b/ruoyi-extend/ruoyi-xxl-job-admin/src/main/java/com/xxl/job/admin/core/cron/CronExpression.java
@@ -1,18 +1,18 @@
/*
* All content copyright Terracotta, Inc., unless otherwise indicated. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
* under the License.
- *
+ *
*/
package com.xxl.job.admin.core.cron;
@@ -31,14 +31,14 @@ import java.util.TimeZone;
import java.util.TreeSet;
/**
- * Provides a parser and evaluator for unix-like cron expressions. Cron
+ * Provides a parser and evaluator for unix-like cron expressions. Cron
* expressions provide the ability to specify complex time combinations such as
- * "At 8:00am every Monday through Friday" or "At 1:30am every
- * last Friday of the month".
+ * "At 8:00am every Monday through Friday" or "At 1:30am every
+ * last Friday of the month".
*
* Cron expressions are comprised of 6 required fields and one optional field
* separated by white space. The fields respectively are described as follows:
- *
+ *
*
*
* Field Name
@@ -98,7 +98,7 @@ import java.util.TreeSet;
*
*
*
- * The '*' character is used to specify all values. For example, "*"
+ * The '*' character is used to specify all values. For example, "*"
* in the minute field means "every minute".
*
* The '?' character is allowed for the day-of-month and day-of-week fields. It
@@ -113,55 +113,55 @@ import java.util.TreeSet;
* Wednesday, and Friday".
*
* The '/' character is used to specify increments. For example "0/15"
- * in the seconds field means "the seconds 0, 15, 30, and 45". And
+ * in the seconds field means "the seconds 0, 15, 30, and 45". And
* "5/15" in the seconds field means "the seconds 5, 20, 35, and
* 50". Specifying '*' before the '/' is equivalent to specifying 0 is
* the value to start with. Essentially, for each field in the expression, there
- * is a set of numbers that can be turned on or off. For seconds and minutes,
+ * is a set of numbers that can be turned on or off. For seconds and minutes,
* the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to
* 31, and for months 0 to 11 (JAN to DEC). The "/" character simply helps you turn
* on every "nth" value in the given set. Thus "7/6" in the
- * month field only turns on month "7", it does NOT mean every 6th
- * month, please note that subtlety.
+ * month field only turns on month "7", it does NOT mean every 6th
+ * month, please note that subtlety.
*
* The 'L' character is allowed for the day-of-month and day-of-week fields.
- * This character is short-hand for "last", but it has different
- * meaning in each of the two fields. For example, the value "L" in
- * the day-of-month field means "the last day of the month" - day 31
- * for January, day 28 for February on non-leap years. If used in the
- * day-of-week field by itself, it simply means "7" or
+ * This character is short-hand for "last", but it has different
+ * meaning in each of the two fields. For example, the value "L" in
+ * the day-of-month field means "the last day of the month" - day 31
+ * for January, day 28 for February on non-leap years. If used in the
+ * day-of-week field by itself, it simply means "7" or
* "SAT". But if used in the day-of-week field after another value, it
* means "the last xxx day of the month" - for example "6L"
- * means "the last friday of the month". You can also specify an offset
- * from the last day of the month, such as "L-3" which would mean the third-to-last
- * day of the calendar month. When using the 'L' option, it is important not to
+ * means "the last friday of the month". You can also specify an offset
+ * from the last day of the month, such as "L-3" which would mean the third-to-last
+ * day of the calendar month. When using the 'L' option, it is important not to
* specify lists, or ranges of values, as you'll get confusing/unexpected results.
*
- * The 'W' character is allowed for the day-of-month field. This character
- * is used to specify the weekday (Monday-Friday) nearest the given day. As an
- * example, if you were to specify "15W" as the value for the
+ * The 'W' character is allowed for the day-of-month field. This character
+ * is used to specify the weekday (Monday-Friday) nearest the given day. As an
+ * example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of
- * the month". So if the 15th is a Saturday, the trigger will fire on
+ * the month". So if the 15th is a Saturday, the trigger will fire on
* Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the
- * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
+ * 16th. If the 15th is a Tuesday, then it will fire on Tuesday the 15th.
* However if you specify "1W" as the value for day-of-month, and the
- * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
- * 'jump' over the boundary of a month's days. The 'W' character can only be
+ * 1st is a Saturday, the trigger will fire on Monday the 3rd, as it will not
+ * 'jump' over the boundary of a month's days. The 'W' character can only be
* specified when the day-of-month is a single day, not a range or list of days.
*
- * The 'L' and 'W' characters can also be combined for the day-of-month
- * expression to yield 'LW', which translates to "last weekday of the
+ * The 'L' and 'W' characters can also be combined for the day-of-month
+ * expression to yield 'LW', which translates to "last weekday of the
* month".
*
* The '#' character is allowed for the day-of-week field. This character is
- * used to specify "the nth" XXX day of the month. For example, the
- * value of "6#3" in the day-of-week field means the third Friday of
- * the month (day 6 = Friday and "#3" = the 3rd one in the month).
- * Other examples: "2#1" = the first Monday of the month and
+ * used to specify "the nth" XXX day of the month. For example, the
+ * value of "6#3" in the day-of-week field means the third Friday of
+ * the month (day 6 = Friday and "#3" = the 3rd one in the month).
+ * Other examples: "2#1" = the first Monday of the month and
* "4#5" = the fifth Wednesday of the month. Note that if you specify
* "#5" and there is not 5 of the given day-of-week in the month, then
* no firing will occur that month. If the '#' character is used, there can
- * only be one expression in the day-of-week field ("3#1,6#3" is
+ * only be one expression in the day-of-week field ("3#1,6#3" is
* not valid, since there are two expressions).
*
*
-
- org.apache.poi
- poi-ooxml
-
-
com.alibaba
easyexcel
-
+
- com.sun.xml.bind
- jaxb-impl
+ org.yaml
+ snakeyaml
@@ -179,6 +173,11 @@
hutool-extra
+
+ com.sun.mail
+ jakarta.mail
+
+
org.projectlombok
lombok
@@ -225,36 +224,27 @@
tlog-xxljob-spring-boot-starter
-
- com.qiniu
- qiniu-java-sdk
- ${qiniu.version}
-
- com.aliyun.oss
- aliyun-sdk-oss
- ${aliyun.oss.version}
+ com.amazonaws
+ aws-java-sdk-s3
+
+
- com.qcloud
- cos_api
- ${qcloud.cos.version}
+ com.aliyun
+ dysmsapi20170525
+
+
+
+ com.tencentcloudapi
+ tencentcloud-sdk-java
- org.slf4j
- slf4j-log4j12
-
-
- org.bouncycastle
- bcprov-jdk15on
+ com.squareup.okio
+ okio
-
- io.minio
- minio
- ${minio.version}
-
diff --git a/ruoyi/src/main/java/com/ruoyi/common/annotation/Anonymous.java b/ruoyi/src/main/java/com/ruoyi/common/annotation/Anonymous.java
new file mode 100644
index 000000000..fe2810083
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/common/annotation/Anonymous.java
@@ -0,0 +1,18 @@
+package com.ruoyi.common.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 匿名访问不鉴权注解
+ *
+ * @author ruoyi
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Anonymous {
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/annotation/CellMerge.java b/ruoyi/src/main/java/com/ruoyi/common/annotation/CellMerge.java
new file mode 100644
index 000000000..4af822eda
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/common/annotation/CellMerge.java
@@ -0,0 +1,24 @@
+package com.ruoyi.common.annotation;
+
+import com.ruoyi.common.excel.CellMergeStrategy;
+
+import java.lang.annotation.*;
+
+/**
+ * excel 列单元格合并(合并列相同项)
+ *
+ * 需搭配 {@link CellMergeStrategy} 策略使用
+ *
+ * @author Lion Li
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface CellMerge {
+
+ /**
+ * col index
+ */
+ int index() default -1;
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
index 6885e95cf..9aa75f7a4 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -108,12 +108,6 @@ public class SysUser extends BaseEntity {
)
private String password;
- @JsonIgnore
- @JsonProperty
- public String getPassword() {
- return password;
- }
-
/**
* 帐号状态(0正常 1停用)
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java b/ruoyi/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
new file mode 100644
index 000000000..04a1bbb8a
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
@@ -0,0 +1,114 @@
+package com.ruoyi.common.excel;
+
+import com.alibaba.excel.metadata.Head;
+import com.alibaba.excel.write.merge.AbstractMergeStrategy;
+import com.ruoyi.common.annotation.CellMerge;
+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;
+import java.util.Map;
+
+/**
+ * 列值重复合并策略
+ *
+ * @author Lion Li
+ */
+@AllArgsConstructor
+@Slf4j
+public class CellMergeStrategy extends AbstractMergeStrategy {
+
+ private List> list;
+ private boolean hasTitle;
+
+ @Override
+ protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
+ List cellList = handle(list, hasTitle);
+ // judge the list is not null
+ if (CollectionUtils.isNotEmpty(cellList)) {
+ // the judge is necessary
+ if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
+ for (CellRangeAddress item : cellList) {
+ sheet.addMergedRegion(item);
+ }
+ }
+ }
+ }
+
+ @SneakyThrows
+ private static List handle(List> list, boolean hasTitle) {
+ List cellList = new ArrayList<>();
+ if (CollectionUtils.isEmpty(list)) {
+ return cellList;
+ }
+ Class> clazz = list.get(0).getClass();
+ Field[] fields = clazz.getDeclaredFields();
+ // 有注解的字段
+ List mergeFields = new ArrayList<>();
+ List mergeFieldsIndex = new ArrayList<>();
+ for (int i = 0; i < fields.length; i++) {
+ Field field = fields[i];
+ if (field.isAnnotationPresent(CellMerge.class)) {
+ CellMerge cm = field.getAnnotation(CellMerge.class);
+ mergeFields.add(field);
+ mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
+ }
+ }
+ // 行合并开始下标
+ int rowIndex = hasTitle ? 1 : 0;
+ Map 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));
+
+ int colNum = mergeFieldsIndex.get(j);
+ if (!map.containsKey(field)) {
+ map.put(field, new RepeatCell(val, i));
+ } else {
+ RepeatCell repeatCell = map.get(field);
+ Object cellValue = repeatCell.getValue();
+ if (cellValue == null || "".equals(cellValue)) {
+ // 空值跳过不合并
+ continue;
+ }
+ if (cellValue != val) {
+ if (i - repeatCell.getCurrent() > 1) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+ }
+ map.put(field, new RepeatCell(val, i));
+ } else if (i == list.size() - 1) {
+ if (i > repeatCell.getCurrent()) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+ }
+ }
+ }
+ }
+ }
+ return cellList;
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class RepeatCell {
+
+ private Object value;
+
+ private int current;
+
+ }
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java b/ruoyi/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java
index ec4d56767..0e60485ad 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/helper/DataPermissionHelper.java
@@ -1,11 +1,11 @@
package com.ruoyi.common.helper;
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.context.model.SaStorage;
import cn.hutool.core.util.ObjectUtil;
-import com.ruoyi.common.utils.ServletUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
-import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@@ -33,11 +33,11 @@ public class DataPermissionHelper {
}
public static Map getContext() {
- HttpServletRequest request = ServletUtils.getRequest();
- Object attribute = request.getAttribute(DATA_PERMISSION_KEY);
+ SaStorage saStorage = SaHolder.getStorage();
+ Object attribute = saStorage.get(DATA_PERMISSION_KEY);
if (ObjectUtil.isNull(attribute)) {
- request.setAttribute(DATA_PERMISSION_KEY, new HashMap<>());
- attribute = request.getAttribute(DATA_PERMISSION_KEY);
+ saStorage.set(DATA_PERMISSION_KEY, new HashMap<>());
+ attribute = saStorage.get(DATA_PERMISSION_KEY);
}
if (attribute instanceof Map) {
return (Map) attribute;
diff --git a/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java b/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java
index 7875c5630..09e1dc757 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java
@@ -1,5 +1,6 @@
package com.ruoyi.common.helper;
+import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.constant.UserConstants;
@@ -13,7 +14,7 @@ import lombok.NoArgsConstructor;
/**
* 登录鉴权助手
- *
+ *
* user_type 为 用户类型 同一个用户表 可以有多种用户类型 例如 pc,app
* deivce 为 设备类型 同一个用户类型 可以有 多种设备类型 例如 web,ios
* 可以组成 用户类型与设备类型多对多的 权限灵活控制
@@ -29,15 +30,13 @@ public class LoginHelper {
public static final String JOIN_CODE = ":";
public static final String LOGIN_USER_KEY = "loginUser";
- private static final ThreadLocal LOGIN_CACHE = new ThreadLocal<>();
-
/**
* 登录系统
*
* @param loginUser 登录用户信息
*/
public static void login(LoginUser loginUser) {
- LOGIN_CACHE.set(loginUser);
+ SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
StpUtil.login(loginUser.getLoginId());
setLoginUser(loginUser);
}
@@ -49,7 +48,7 @@ public class LoginHelper {
* @param loginUser 登录用户信息
*/
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
- LOGIN_CACHE.set(loginUser);
+ SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
StpUtil.login(loginUser.getLoginId(), deviceType.getDevice());
setLoginUser(loginUser);
}
@@ -65,18 +64,13 @@ public class LoginHelper {
* 获取用户(多级缓存)
*/
public static LoginUser getLoginUser() {
- LoginUser loginUser = LOGIN_CACHE.get();
+ LoginUser loginUser = (LoginUser) SaHolder.getStorage().get(LOGIN_USER_KEY);
if (loginUser != null) {
return loginUser;
}
- return (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
- }
-
- /**
- * 清除一级缓存 防止内存问题
- */
- public static void clearCache() {
- LOGIN_CACHE.remove();
+ loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
+ SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
+ return loginUser;
}
/**
diff --git a/ruoyi/src/main/java/com/ruoyi/common/jackson/SensitiveJsonSerializer.java b/ruoyi/src/main/java/com/ruoyi/common/jackson/SensitiveJsonSerializer.java
index 55c4e6a7d..404f393fe 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/jackson/SensitiveJsonSerializer.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/jackson/SensitiveJsonSerializer.java
@@ -27,9 +27,9 @@ public class SensitiveJsonSerializer extends JsonSerializer implements C
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
SensitiveService sensitiveService = SpringUtils.getBean(SensitiveService.class);
if (sensitiveService.isSensitive()) {
- gen.writeString(value);
- } else {
gen.writeString(strategy.desensitizer().apply(value));
+ } else {
+ gen.writeString(value);
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/email/MailUtils.java b/ruoyi/src/main/java/com/ruoyi/common/utils/email/MailUtils.java
new file mode 100644
index 000000000..32a3f8252
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/email/MailUtils.java
@@ -0,0 +1,468 @@
+package com.ruoyi.common.utils.email;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.CharUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.mail.*;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import javax.mail.Authenticator;
+import javax.mail.Session;
+import java.io.File;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 邮件工具类
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MailUtils {
+
+ private static final MailAccount ACCOUNT = SpringUtils.getBean(MailAccount.class);
+
+ /**
+ * 获取邮件发送实例
+ */
+ public static MailAccount getMailAccount() {
+ return ACCOUNT;
+ }
+
+ /**
+ * 获取邮件发送实例 (自定义发送人以及授权码)
+ *
+ * @param user 发送人
+ * @param pass 授权码
+ */
+ public static MailAccount getMailAccount(String from, String user, String pass) {
+ ACCOUNT.setFrom(StringUtils.blankToDefault(from, ACCOUNT.getFrom()));
+ ACCOUNT.setUser(StringUtils.blankToDefault(user, ACCOUNT.getUser()));
+ ACCOUNT.setPass(StringUtils.blankToDefault(pass, ACCOUNT.getPass()));
+ return ACCOUNT;
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送文本邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendText(String to, String subject, String content, File... files) {
+ return send(to, subject, content, false, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, File... files) {
+ return send(to, subject, content, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送文本邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String sendText(Collection tos, String subject, String content, File... files) {
+ return send(tos, subject, content, false, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection tos, String subject, String content, File... files) {
+ return send(tos, subject, content, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(Collection tos, String subject, String content, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(getMailAccount(), true, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件认证对象
+ * @param to 收件人,多个收件人逗号或者分号隔开
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(MailAccount mailAccount, Collection tos, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, null, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(String to, String subject, String content, Map imageMap, File... files) {
+ return send(to, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(String to, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送单个或多个收件人
+ * 多个收件人、抄送人、密送人可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ *
+ * @param to 收件人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param cc 抄送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param bcc 密送人,可以使用逗号“,”分隔,也可以通过分号“;”分隔
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(String to, String cc, String bcc, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(splitAddress(to), splitAddress(cc), splitAddress(bcc), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送HTML邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String sendHtml(Collection tos, String subject, String content, Map imageMap, File... files) {
+ return send(tos, subject, content, imageMap, true, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ */
+ public static String send(Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 使用配置文件中设置的账户发送邮件,发送给多人
+ *
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.0.3
+ */
+ public static String send(Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(getMailAccount(), true, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------------- Custom MailAccount
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件认证对象
+ * @param to 收件人,多个收件人逗号或者分号隔开
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 3.2.0
+ */
+ public static String send(MailAccount mailAccount, String to, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, splitAddress(to), subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, String subject, String content, Map imageMap, boolean isHtml, File... files) {
+ return send(mailAccount, tos, null, null, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:$IMAGE_PLACEHOLDER
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ public static String send(MailAccount mailAccount, Collection tos, Collection ccs, Collection bccs, String subject, String content, Map imageMap,
+ boolean isHtml, File... files) {
+ return send(mailAccount, false, tos, ccs, bccs, subject, content, imageMap, isHtml, files);
+ }
+
+ /**
+ * 根据配置文件,获取邮件客户端会话
+ *
+ * @param mailAccount 邮件账户配置
+ * @param isSingleton 是否单例(全局共享会话)
+ * @return {@link Session}
+ * @since 5.5.7
+ */
+ public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
+ Authenticator authenticator = null;
+ if (mailAccount.isAuth()) {
+ authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
+ }
+
+ return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
+ : Session.getInstance(mailAccount.getSmtpProps(), authenticator);
+ }
+
+ // ------------------------------------------------------------------------------------------------------------------------ Private method start
+
+ /**
+ * 发送邮件给多人
+ *
+ * @param mailAccount 邮件帐户信息
+ * @param useGlobalSession 是否全局共享Session
+ * @param tos 收件人列表
+ * @param ccs 抄送人列表,可以为null或空
+ * @param bccs 密送人列表,可以为null或空
+ * @param subject 标题
+ * @param content 正文
+ * @param imageMap 图片与占位符,占位符格式为cid:${cid}
+ * @param isHtml 是否为HTML格式
+ * @param files 附件列表
+ * @return message-id
+ * @since 4.6.3
+ */
+ private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection tos, Collection ccs, Collection bccs, String subject, String content,
+ Map imageMap, boolean isHtml, File... files) {
+ final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
+
+ // 可选抄送人
+ if (CollUtil.isNotEmpty(ccs)) {
+ mail.setCcs(ccs.toArray(new String[0]));
+ }
+ // 可选密送人
+ if (CollUtil.isNotEmpty(bccs)) {
+ mail.setBccs(bccs.toArray(new String[0]));
+ }
+
+ mail.setTos(tos.toArray(new String[0]));
+ mail.setTitle(subject);
+ mail.setContent(content);
+ mail.setHtml(isHtml);
+ mail.setFiles(files);
+
+ // 图片
+ if (MapUtil.isNotEmpty(imageMap)) {
+ for (Map.Entry entry : imageMap.entrySet()) {
+ mail.addImage(entry.getKey(), entry.getValue());
+ // 关闭流
+ IoUtil.close(entry.getValue());
+ }
+ }
+
+ return mail.send();
+ }
+
+ /**
+ * 将多个联系人转为列表,分隔符为逗号或者分号
+ *
+ * @param addresses 多个联系人,如果为空返回null
+ * @return 联系人列表
+ */
+ private static List splitAddress(String addresses) {
+ if (StrUtil.isBlank(addresses)) {
+ return null;
+ }
+
+ List result;
+ if (StrUtil.contains(addresses, CharUtil.COMMA)) {
+ result = StrUtil.splitTrim(addresses, CharUtil.COMMA);
+ } else if (StrUtil.contains(addresses, ';')) {
+ result = StrUtil.splitTrim(addresses, ';');
+ } else {
+ result = CollUtil.newArrayList(addresses);
+ }
+ return result;
+ }
+ // ------------------------------------------------------------------------------------------------------------------------ Private method end
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java b/ruoyi/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java
new file mode 100644
index 000000000..6ca97fe60
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/file/MimeTypeUtils.java
@@ -0,0 +1,40 @@
+package com.ruoyi.common.utils.file;
+
+/**
+ * 媒体类型工具类
+ *
+ * @author ruoyi
+ */
+public class MimeTypeUtils {
+ public static final String IMAGE_PNG = "image/png";
+
+ public static final String IMAGE_JPG = "image/jpg";
+
+ public static final String IMAGE_JPEG = "image/jpeg";
+
+ public static final String IMAGE_BMP = "image/bmp";
+
+ public static final String IMAGE_GIF = "image/gif";
+
+ public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+ public static final String[] FLASH_EXTENSION = {"swf", "flv"};
+
+ public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+ "asf", "rm", "rmvb"};
+
+ public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+ public static final String[] DEFAULT_ALLOWED_EXTENSION = {
+ // 图片
+ "bmp", "gif", "jpg", "jpeg", "png",
+ // word excel powerpoint
+ "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+ // 压缩文件
+ "rar", "zip", "gz", "bz2",
+ // 视频格式
+ "mp4", "avi", "rmvb",
+ // pdf
+ "pdf"};
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
index 36dc67fa1..e6a67b26b 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -1,9 +1,17 @@
package com.ruoyi.common.utils.poi;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelWriter;
+import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
+import com.alibaba.excel.write.metadata.WriteSheet;
+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;
@@ -16,7 +24,10 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
import java.util.List;
+import java.util.Map;
/**
* Excel相关处理
@@ -69,27 +80,125 @@ public class ExcelUtil {
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
- * @return 结果
+ * @param clazz 实体类
+ * @param response 响应体
*/
public static void exportExcel(List list, String sheetName, Class clazz, HttpServletResponse response) {
+ exportExcel(list, sheetName, clazz, false, response);
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param merge 是否合并单元格
+ * @param response 响应体
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, boolean merge, HttpServletResponse response) {
try {
- String filename = encodingFilename(sheetName);
- response.reset();
- FileUtils.setAttachmentResponseHeader(response, filename);
- response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+ resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
- EasyExcel.write(os, clazz)
+ ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
.autoCloseStream(false)
// 自动适配
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert())
- .sheet(sheetName).doWrite(list);
+ .sheet(sheetName);
+ if (merge) {
+ // 合并处理器
+ builder.registerWriteHandler(new CellMergeStrategy(list, true));
+ }
+ builder.doWrite(list);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
}
+ /**
+ * 单表多数据模板导出 模板格式为 {.属性}
+ *
+ * @param filename 文件名
+ * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
+ * 例如: excel/temp.xlsx
+ * 重点: 模板文件必须放置到启动类对应的 resource 目录下
+ * @param data 模板需要的数据
+ */
+ public static void exportTemplate(List data, String filename, String templatePath, HttpServletResponse response) {
+ try {
+ resetResponse(filename, response);
+ ClassPathResource templateResource = new ClassPathResource(templatePath);
+ ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
+ .withTemplate(templateResource.getStream())
+ .autoCloseStream(false)
+ // 大数值自动转换 防止失真
+ .registerConverter(new ExcelBigNumberConvert())
+ .build();
+ WriteSheet writeSheet = EasyExcel.writerSheet().build();
+ if (CollUtil.isEmpty(data)) {
+ throw new IllegalArgumentException("数据为空");
+ }
+ // 单表多数据导出 模板格式为 {.属性}
+ for (Object d : data) {
+ excelWriter.fill(d, writeSheet);
+ }
+ excelWriter.finish();
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 多表多数据模板导出 模板格式为 {key.属性}
+ *
+ * @param filename 文件名
+ * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
+ * 例如: excel/temp.xlsx
+ * 重点: 模板文件必须放置到启动类对应的 resource 目录下
+ * @param data 模板需要的数据
+ */
+ public static void exportTemplateMultiList(Map data, String filename, String templatePath, HttpServletResponse response) {
+ try {
+ resetResponse(filename, response);
+ ClassPathResource templateResource = new ClassPathResource(templatePath);
+ ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
+ .withTemplate(templateResource.getStream())
+ .autoCloseStream(false)
+ // 大数值自动转换 防止失真
+ .registerConverter(new ExcelBigNumberConvert())
+ .build();
+ WriteSheet writeSheet = EasyExcel.writerSheet().build();
+ if (CollUtil.isEmpty(data)) {
+ throw new IllegalArgumentException("数据为空");
+ }
+ for (Map.Entry map : data.entrySet()) {
+ // 设置列表后续还有数据
+ FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
+ if (map.getValue() instanceof Collection) {
+ // 多表导出必须使用 FillWrapper
+ excelWriter.fill(new FillWrapper(map.getKey(), (Collection>) map.getValue()), fillConfig, writeSheet);
+ } else {
+ excelWriter.fill(map.getValue(), writeSheet);
+ }
+ }
+ excelWriter.finish();
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 重置响应体
+ */
+ private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
+ String filename = encodingFilename(sheetName);
+ response.reset();
+ FileUtils.setAttachmentResponseHeader(response, filename);
+ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
+ }
+
/**
* 解析导出值 0=男,1=女,2=未知
*
@@ -103,7 +212,7 @@ public class ExcelUtil {
String[] convertSource = converterExp.split(",");
for (String item : convertSource) {
String[] itemArray = item.split("=");
- if (StringUtils.containsAny(separator, propertyValue)) {
+ if (StringUtils.containsAny(propertyValue, separator)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1] + separator);
@@ -132,7 +241,7 @@ public class ExcelUtil {
String[] convertSource = converterExp.split(",");
for (String item : convertSource) {
String[] itemArray = item.split("=");
- if (StringUtils.containsAny(separator, propertyValue)) {
+ if (StringUtils.containsAny(propertyValue, separator)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[1].equals(value)) {
propertyString.append(itemArray[0] + separator);
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java b/ruoyi/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
index 5a15d46b7..7ed3b2827 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
@@ -6,11 +6,11 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.redisson.api.*;
+import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -107,7 +107,7 @@ public class RedisUtils {
} catch (Exception e) {
long timeToLive = bucket.remainTimeToLive();
bucket.set(value);
- bucket.expire(timeToLive, TimeUnit.MILLISECONDS);
+ bucket.expire(Duration.ofMillis(timeToLive));
}
} else {
bucket.set(value);
@@ -119,13 +119,12 @@ public class RedisUtils {
*
* @param key 缓存的键值
* @param value 缓存的值
- * @param timeout 时间
- * @param timeUnit 时间颗粒度
+ * @param duration 时间
*/
- public static void setCacheObject(final String key, final T value, final long timeout, final TimeUnit timeUnit) {
+ public static void setCacheObject(final String key, final T value, final Duration duration) {
RBucket result = CLIENT.getBucket(key);
result.set(value);
- result.expire(timeout, timeUnit);
+ result.expire(duration);
}
/**
@@ -149,20 +148,19 @@ public class RedisUtils {
* @return true=设置成功;false=设置失败
*/
public static boolean expire(final String key, final long timeout) {
- return expire(key, timeout, TimeUnit.SECONDS);
+ return expire(key, Duration.ofSeconds(timeout));
}
/**
* 设置有效时间
*
- * @param key Redis键
- * @param timeout 超时时间
- * @param unit 时间单位
+ * @param key Redis键
+ * @param duration 超时时间
* @return true=设置成功;false=设置失败
*/
- public static boolean expire(final String key, final long timeout, final TimeUnit unit) {
+ public static boolean expire(final String key, final Duration duration) {
RBucket rBucket = CLIENT.getBucket(key);
- return rBucket.expire(timeout, unit);
+ return rBucket.expire(duration);
}
/**
@@ -366,6 +364,50 @@ public class RedisUtils {
return rMap.getAll(hKeys);
}
+ /**
+ * 设置原子值
+ *
+ * @param key Redis键
+ * @param value 值
+ */
+ public static void setAtomicValue(String key, long value) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ atomic.set(value);
+ }
+
+ /**
+ * 获取原子值
+ *
+ * @param key Redis键
+ * @return 当前值
+ */
+ public static long getAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.get();
+ }
+
+ /**
+ * 递增原子值
+ *
+ * @param key Redis键
+ * @return 当前值
+ */
+ public static long incrAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.incrementAndGet();
+ }
+
+ /**
+ * 递减原子值
+ *
+ * @param key Redis键
+ * @return 当前值
+ */
+ public static long decrAtomicValue(String key) {
+ RAtomicLong atomic = CLIENT.getAtomicLong(key);
+ return atomic.decrementAndGet();
+ }
+
/**
* 获得缓存的基本对象列表
*
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/MailController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/MailController.java
new file mode 100644
index 000000000..6dabe5132
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/MailController.java
@@ -0,0 +1,48 @@
+package com.ruoyi.demo.controller;
+
+import com.ruoyi.common.core.domain.R;
+import com.ruoyi.common.utils.email.MailUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.File;
+
+
+/**
+ * 邮件发送案例
+ *
+ * @author Michelle.Chung
+ */
+@Validated
+@Api(value = "邮件发送案例", tags = {"邮件发送案例"})
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/demo/mail")
+public class MailController {
+
+ @ApiOperation("发送邮件")
+ @GetMapping("/sendSimpleMessage")
+ public R sendSimpleMessage(@ApiParam("接收人") String to,
+ @ApiParam("标题") String subject,
+ @ApiParam("内容") String text) {
+ MailUtils.sendText(to, subject, text);
+ return R.ok();
+ }
+
+ @ApiOperation("发送邮件(带附件)")
+ @GetMapping("/sendMessageWithAttachment")
+ public R sendMessageWithAttachment(@ApiParam("接收人") String to,
+ @ApiParam("标题") String subject,
+ @ApiParam("内容") String text,
+ @ApiParam("附件路径") String filePath) {
+ MailUtils.sendText(to, subject, text, new File(filePath));
+ return R.ok();
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java
index 98cca18c0..9fc93074f 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/RedisCacheController.java
@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
/**
* spring-cache 演示案例
@@ -87,7 +87,7 @@ public class RedisCacheController {
@GetMapping("/test6")
public R test6(String key, String value) {
RedisUtils.setCacheObject(key, value);
- boolean flag = RedisUtils.expire(key, 10, TimeUnit.SECONDS);
+ boolean flag = RedisUtils.expire(key, Duration.ofSeconds(10));
System.out.println("***********" + flag);
try {
Thread.sleep(11 * 1000);
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java
new file mode 100644
index 000000000..b92078624
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java
@@ -0,0 +1,72 @@
+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 io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 短信演示案例
+ * 请先阅读文档 否则无法使用
+ *
+ * @author Lion Li
+ * @version 4.2.0
+ */
+@Validated
+@Api(value = "短信演示案例", tags = {"短信演示案例"})
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/demo/sms")
+public class SmsController {
+
+ private final SmsProperties smsProperties;
+// private final SmsTemplate smsTemplate; // 可以使用spring注入
+// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
+
+ @ApiOperation("发送短信Aliyun")
+ @GetMapping("/sendAliyun")
+ public R sendAliyun(@ApiParam("电话号") String phones,
+ @ApiParam("模板ID") String templateId) {
+ if (!smsProperties.getEnabled()) {
+ return R.fail("当前系统没有开启短信功能!");
+ }
+ if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
+ return R.fail("阿里云依赖未引入!");
+ }
+ SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
+ Map map = new HashMap<>(1);
+ map.put("code", "1234");
+ Object send = smsTemplate.send(phones, templateId, map);
+ return R.ok(send);
+ }
+
+ @ApiOperation("发送短信Tencent")
+ @GetMapping("/sendTencent")
+ public R sendTencent(@ApiParam("电话号") String phones,
+ @ApiParam("模板ID") String templateId) {
+ if (!smsProperties.getEnabled()) {
+ return R.fail("当前系统没有开启短信功能!");
+ }
+ if (!SpringUtils.containsBean("tencentSmsTemplate")) {
+ return R.fail("腾讯云依赖未引入!");
+ }
+ SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
+ Map map = new HashMap<>(1);
+// map.put("2", "测试测试");
+ map.put("1", "1234");
+ Object send = smsTemplate.send(phones, templateId, map);
+ return R.ok(send);
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/TestDemoController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestDemoController.java
index d1f994e5d..9f00c0770 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/controller/TestDemoController.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestDemoController.java
@@ -29,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
+import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -70,7 +71,7 @@ public class TestDemoController extends BaseController {
@ApiOperation("导入测试-校验")
@ApiImplicitParams({
- @ApiImplicitParam(name = "file", value = "导入文件", dataType = "java.io.File", required = true),
+ @ApiImplicitParam(name = "file", value = "导入文件", paramType = "query", dataTypeClass = File.class, required = true),
})
@Log(title = "测试单表", businessType = BusinessType.IMPORT)
@SaCheckPermission("demo:demo:import")
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
new file mode 100644
index 000000000..a318b4658
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
@@ -0,0 +1,102 @@
+package com.ruoyi.demo.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+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 javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 测试Excel功能
+ *
+ * @author Lion Li
+ */
+@Api(value = "测试Excel功能", tags = {"测试Excel功能"})
+@RestController
+@RequestMapping("/demo/excel")
+public class TestExcelController {
+
+ /**
+ * 单列表多数据
+ */
+ @ApiOperation(value = "单列表多数据")
+ @GetMapping("/exportTemplateOne")
+ public void exportTemplateOne(HttpServletResponse response) {
+ Map map = new HashMap<>();
+ map.put("title","单列表多数据");
+ map.put("test1","数据测试1");
+ map.put("test2","数据测试2");
+ map.put("test3","数据测试3");
+ map.put("test4","数据测试4");
+ map.put("testTest","666");
+ List list = new ArrayList<>();
+ list.add(new TestObj("单列表测试1", "列表测试1", "列表测试2", "列表测试3", "列表测试4"));
+ list.add(new TestObj("单列表测试2", "列表测试5", "列表测试6", "列表测试7", "列表测试8"));
+ list.add(new TestObj("单列表测试3", "列表测试9", "列表测试10", "列表测试11", "列表测试12"));
+ ExcelUtil.exportTemplate(CollUtil.newArrayList(map,list),"单列表.xlsx", "excel/单列表.xlsx", response);
+ }
+
+ /**
+ * 多列表多数据
+ */
+ @ApiOperation(value = "多列表多数据")
+ @GetMapping("/exportTemplateMuliti")
+ public void exportTemplateMuliti(HttpServletResponse response) {
+ Map map = new HashMap<>();
+ map.put("title1","标题1");
+ map.put("title2","标题2");
+ map.put("title3","标题3");
+ map.put("title4","标题4");
+ map.put("author","Lion Li");
+ List list1 = new ArrayList<>();
+ list1.add(new TestObj1("list1测试1", "list1测试2", "list1测试3"));
+ list1.add(new TestObj1("list1测试4", "list1测试5", "list1测试6"));
+ list1.add(new TestObj1("list1测试7", "list1测试8", "list1测试9"));
+ List list2 = new ArrayList<>();
+ list2.add(new TestObj1("list2测试1", "list2测试2", "list2测试3"));
+ list2.add(new TestObj1("list2测试4", "list2测试5", "list2测试6"));
+ List list3 = new ArrayList<>();
+ list3.add(new TestObj1("list3测试1", "list3测试2", "list3测试3"));
+ List list4 = new ArrayList<>();
+ list4.add(new TestObj1("list4测试1", "list4测试2", "list4测试3"));
+ list4.add(new TestObj1("list4测试4", "list4测试5", "list4测试6"));
+ list4.add(new TestObj1("list4测试7", "list4测试8", "list4测试9"));
+ list4.add(new TestObj1("list4测试10", "list4测试11", "list4测试12"));
+ Map multiListMap = new HashMap<>();
+ multiListMap.put("map",map);
+ multiListMap.put("data1",list1);
+ multiListMap.put("data2",list2);
+ multiListMap.put("data3",list3);
+ multiListMap.put("data4",list4);
+ ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class TestObj1 {
+ private String test1;
+ private String test2;
+ private String test3;
+ }
+
+ @Data
+ @AllArgsConstructor
+ static class TestObj {
+ private String name;
+ private String list1;
+ private String list2;
+ private String list3;
+ private String list4;
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/domain/bo/TestDemoBo.java b/ruoyi/src/main/java/com/ruoyi/demo/domain/bo/TestDemoBo.java
index 5c41204d7..61c10ab16 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/domain/bo/TestDemoBo.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/domain/bo/TestDemoBo.java
@@ -49,7 +49,7 @@ public class TestDemoBo extends BaseEntity {
*/
@ApiModelProperty("排序号")
@NotNull(message = "排序号不能为空", groups = {AddGroup.class, EditGroup.class})
- private Long orderNum;
+ private Integer orderNum;
/**
* key键
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/TestDemoVo.java b/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/TestDemoVo.java
index ef26ff325..1e896500d 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/TestDemoVo.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/TestDemoVo.java
@@ -48,7 +48,7 @@ public class TestDemoVo {
*/
@ExcelProperty(value = "排序号")
@ApiModelProperty("排序号")
- private Long orderNum;
+ private Integer orderNum;
/**
* key键
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/mapper/TestDemoMapper.java b/ruoyi/src/main/java/com/ruoyi/demo/mapper/TestDemoMapper.java
index b1b80d26e..11a3d50e2 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/mapper/TestDemoMapper.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/mapper/TestDemoMapper.java
@@ -54,5 +54,5 @@ public interface TestDemoMapper extends BaseMapperPlus idList);
+ int deleteBatchIds(@Param(Constants.COLL) Collection> idList);
}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java b/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
index aedc4431c..b77acb40e 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
@@ -25,9 +25,9 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import java.time.Duration;
import java.util.Collection;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
/**
* 防止重复提交(参考美团GTIS防重系统)
@@ -66,7 +66,7 @@ public class RepeatSubmitAspect {
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + url + submitKey;
String key = RedisUtils.getCacheObject(cacheRepeatKey);
if (key == null) {
- RedisUtils.setCacheObject(cacheRepeatKey, "", interval, TimeUnit.MILLISECONDS);
+ RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/JacksonConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/JacksonConfig.java
index a5d637165..eace537a5 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/config/JacksonConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/JacksonConfig.java
@@ -1,17 +1,14 @@
package com.ruoyi.framework.config;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.ruoyi.framework.jackson.BigNumberSerializer;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -28,23 +25,22 @@ import java.util.TimeZone;
@Configuration
public class JacksonConfig {
- @Primary
@Bean
- public ObjectMapper getObjectMapper(Jackson2ObjectMapperBuilder builder, JacksonProperties jacksonProperties) {
- ObjectMapper objectMapper = builder.createXmlMapper(false).build();
- // 全局配置序列化返回 JSON 处理
- SimpleModule simpleModule = new SimpleModule();
- simpleModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
- simpleModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
- simpleModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
- simpleModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jacksonProperties.getDateFormat());
- simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
- simpleModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
- objectMapper.registerModule(simpleModule);
- objectMapper.setTimeZone(TimeZone.getDefault());
- log.info("初始化 jackson 配置");
- return objectMapper;
+ public Jackson2ObjectMapperBuilderCustomizer customizer() {
+ return builder -> {
+ // 全局配置序列化返回 JSON 处理
+ JavaTimeModule javaTimeModule = new JavaTimeModule();
+ javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
+ javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
+ javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
+ javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
+ javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
+ builder.modules(javaTimeModule);
+ builder.timeZone(TimeZone.getDefault());
+ log.info("初始化 jackson 配置");
+ };
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/MailConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/MailConfig.java
new file mode 100644
index 000000000..20769aa19
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/MailConfig.java
@@ -0,0 +1,35 @@
+package com.ruoyi.framework.config;
+
+import cn.hutool.extra.mail.MailAccount;
+import com.ruoyi.framework.config.properties.MailProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * JavaMail 配置
+ *
+ * @author Michelle.Chung
+ */
+@Configuration
+public class MailConfig {
+
+ @Bean
+ @ConditionalOnProperty(value = "mail.enabled", havingValue = "true")
+ public MailAccount mailAccount(MailProperties mailProperties) {
+ MailAccount account = new MailAccount();
+ account.setHost(mailProperties.getHost());
+ account.setPort(mailProperties.getPort());
+ account.setAuth(mailProperties.getAuth());
+ account.setFrom(mailProperties.getFrom());
+ account.setUser(mailProperties.getUser());
+ account.setPass(mailProperties.getPass());
+ account.setSocketFactoryPort(mailProperties.getPort());
+ account.setStarttlsEnable(mailProperties.getStarttlsEnable());
+ account.setSslEnable(mailProperties.getSslEnable());
+ account.setTimeout(mailProperties.getTimeout());
+ account.setConnectionTimeout(mailProperties.getConnectionTimeout());
+ return account;
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/RedisConfig.java
index da18cfd17..38a871895 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/config/RedisConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/RedisConfig.java
@@ -1,28 +1,25 @@
package com.ruoyi.framework.config;
import cn.hutool.core.util.ObjectUtil;
-import com.ruoyi.common.utils.StringUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.framework.config.properties.RedissonProperties;
import lombok.extern.slf4j.Slf4j;
-import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
-import org.redisson.config.Config;
import org.redisson.spring.cache.CacheConfig;
import org.redisson.spring.cache.RedissonSpringCacheManager;
+import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* redis配置
@@ -32,72 +29,49 @@ import java.util.stream.Collectors;
@Slf4j
@Configuration
@EnableCaching
+@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig extends CachingConfigurerSupport {
- private static final String REDIS_PROTOCOL_PREFIX = "redis://";
- private static final String REDISS_PROTOCOL_PREFIX = "rediss://";
-
- @Autowired
- private RedisProperties redisProperties;
-
@Autowired
private RedissonProperties redissonProperties;
- @Primary
- @Bean(destroyMethod = "shutdown")
- public RedissonClient redisson() {
- String prefix = REDIS_PROTOCOL_PREFIX;
- if (redisProperties.isSsl()) {
- prefix = REDISS_PROTOCOL_PREFIX;
- }
- Config config = new Config();
- config.setThreads(redissonProperties.getThreads())
- .setNettyThreads(redissonProperties.getNettyThreads())
- .setCodec(JsonJacksonCodec.INSTANCE);
+ @Autowired
+ private ObjectMapper objectMapper;
- RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
- if (ObjectUtil.isNotNull(singleServerConfig)) {
- // 使用单机模式
- config.useSingleServer()
- .setAddress(prefix + redisProperties.getHost() + ":" + redisProperties.getPort())
- .setConnectTimeout(((Long) redisProperties.getTimeout().toMillis()).intValue())
- .setDatabase(redisProperties.getDatabase())
- .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) ? redisProperties.getPassword() : null)
- .setTimeout(singleServerConfig.getTimeout())
- .setClientName(singleServerConfig.getClientName())
- .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
- .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
- .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
- .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
- }
- // 集群配置方式 参考下方注释
- RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
- if (ObjectUtil.isNotNull(clusterServersConfig)) {
- // 使用集群模式
- String finalPrefix = prefix;
- List nodes = redisProperties.getCluster().getNodes()
- .stream()
- .map(node -> finalPrefix + node)
- .collect(Collectors.toList());
-
- config.useClusterServers()
- .setConnectTimeout(((Long) redisProperties.getTimeout().toMillis()).intValue())
- .setPassword(StringUtils.isNotBlank(redisProperties.getPassword()) ? redisProperties.getPassword() : null)
- .setTimeout(clusterServersConfig.getTimeout())
- .setClientName(clusterServersConfig.getClientName())
- .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
- .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
- .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
- .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
- .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
- .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
- .setReadMode(clusterServersConfig.getReadMode())
- .setSubscriptionMode(clusterServersConfig.getSubscriptionMode())
- .setNodeAddresses(nodes);
- }
- RedissonClient redissonClient = Redisson.create(config);
- log.info("初始化 redis 配置");
- return redissonClient;
+ @Bean
+ public RedissonAutoConfigurationCustomizer redissonCustomizer() {
+ return config -> {
+ config.setThreads(redissonProperties.getThreads())
+ .setNettyThreads(redissonProperties.getNettyThreads())
+ .setCodec(new JsonJacksonCodec(objectMapper));
+ RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
+ if (ObjectUtil.isNotNull(singleServerConfig)) {
+ // 使用单机模式
+ config.useSingleServer()
+ .setTimeout(singleServerConfig.getTimeout())
+ .setClientName(singleServerConfig.getClientName())
+ .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
+ .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
+ .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
+ .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
+ }
+ // 集群配置方式 参考下方注释
+ RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
+ if (ObjectUtil.isNotNull(clusterServersConfig)) {
+ config.useClusterServers()
+ .setTimeout(clusterServersConfig.getTimeout())
+ .setClientName(clusterServersConfig.getClientName())
+ .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
+ .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
+ .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
+ .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
+ .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
+ .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
+ .setReadMode(clusterServersConfig.getReadMode())
+ .setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
+ }
+ log.info("初始化 redis 配置");
+ };
}
/**
@@ -112,7 +86,7 @@ public class RedisConfig extends CachingConfigurerSupport {
cacheConfig.setMaxSize(group.getMaxSize());
config.put(group.getGroupId(), cacheConfig);
}
- return new RedissonSpringCacheManager(redissonClient, config, JsonJacksonCodec.INSTANCE);
+ return new RedissonSpringCacheManager(redissonClient, config, new JsonJacksonCodec(objectMapper));
}
/**
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/SaTokenConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/SaTokenConfig.java
index 671400f6f..a5be2678e 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/config/SaTokenConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/SaTokenConfig.java
@@ -2,11 +2,12 @@ package com.ruoyi.framework.config;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.interceptor.SaRouteInterceptor;
-import cn.dev33.satoken.jwt.StpLogicJwtForStyle;
+import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
-import com.ruoyi.common.helper.LoginHelper;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.framework.config.properties.ExcludeUrlProperties;
import com.ruoyi.framework.config.properties.SecurityProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -15,9 +16,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
/**
* sa-token 配置
*
@@ -37,12 +35,14 @@ public class SaTokenConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器,自定义验证规则
registry.addInterceptor(new SaRouteInterceptor((request, response, handler) -> {
+ ExcludeUrlProperties excludeUrlProperties = SpringUtils.getBean(ExcludeUrlProperties.class);
// 登录验证 -- 排除多个路径
SaRouter
// 获取所有的
.match("/**")
// 排除下不需要拦截的
.notMatch(securityProperties.getExcludes())
+ .notMatch(excludeUrlProperties.getExcludes())
// 对未排除的路径进行检查
.check(() -> {
// 检查是否登录 是否有token
@@ -55,20 +55,14 @@ public class SaTokenConfig implements WebMvcConfigurer {
// }
});
- }) {
- @SuppressWarnings("all")
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- LoginHelper.clearCache();
- }
- }).addPathPatterns("/**");
+ })).addPathPatterns("/**");
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
@Bean
public StpLogic getStpLogicJwt() {
- // Sa-Token 整合 jwt (Style模式)
- return new StpLogicJwtForStyle();
+ // Sa-Token 整合 jwt (简单模式)
+ return new StpLogicJwtForSimple();
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/SwaggerConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/SwaggerConfig.java
index 22ef7c1b5..2424981c1 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/config/SwaggerConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/SwaggerConfig.java
@@ -109,7 +109,7 @@ public class SwaggerConfig {
* 安全模式,这里指定token通过Authorization头请求头传递
*/
private List securitySchemes() {
- List apiKeyList = new ArrayList();
+ List apiKeyList = new ArrayList<>();
String header = saTokenConfig.getTokenName();
apiKeyList.add(new ApiKey(header, header, In.HEADER.toValue()));
return apiKeyList;
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
index ebf236c87..a85ad1e57 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
@@ -33,8 +33,8 @@ public class ThreadPoolConfig {
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
- executor.setMaxPoolSize(core);
- executor.setCorePoolSize(core * 2);
+ executor.setCorePoolSize(core);
+ executor.setMaxPoolSize(core * 2);
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/UndertowConfig.java b/ruoyi/src/main/java/com/ruoyi/framework/config/UndertowConfig.java
new file mode 100644
index 000000000..64e745a86
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/UndertowConfig.java
@@ -0,0 +1,30 @@
+package com.ruoyi.framework.config;
+
+import io.undertow.server.DefaultByteBufferPool;
+import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
+import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Undertow 自定义配置
+ *
+ * @author Lion Li
+ */
+@Configuration
+public class UndertowConfig implements WebServerFactoryCustomizer {
+
+ /**
+ * 设置 Undertow 的 websocket 缓冲池
+ */
+ @Override
+ public void customize(UndertowServletWebServerFactory factory) {
+ // 默认不直接分配内存 如果项目中使用了 websocket 建议直接分配
+ factory.addDeploymentInfoCustomizers(deploymentInfo -> {
+ WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
+ webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 512));
+ deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
+ });
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/properties/ExcludeUrlProperties.java b/ruoyi/src/main/java/com/ruoyi/framework/config/properties/ExcludeUrlProperties.java
new file mode 100644
index 000000000..b4e3eae6d
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/properties/ExcludeUrlProperties.java
@@ -0,0 +1,61 @@
+package com.ruoyi.framework.config.properties;
+
+import cn.hutool.core.util.ReUtil;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import lombok.Getter;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import java.util.*;
+import java.util.regex.Pattern;
+
+/**
+ * 设置注解允许匿名访问的url
+ *
+ * @author Lion Li
+ */
+@Lazy
+@Component
+public class ExcludeUrlProperties implements InitializingBean {
+
+ private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+ @Getter
+ private final List excludes = new ArrayList<>();
+
+ @Override
+ public void afterPropertiesSet() {
+ String asterisk = "*";
+ RequestMappingHandlerMapping mapping = SpringUtils.getBean(RequestMappingHandlerMapping.class);
+ Map map = mapping.getHandlerMethods();
+
+ map.keySet().forEach(info -> {
+ HandlerMethod handlerMethod = map.get(info);
+
+ // 获取方法上边的注解 替代path variable 为 *
+ Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
+ Optional.ofNullable(method).ifPresent(anonymous -> {
+ Set patterns = info.getPatternsCondition().getPatterns();
+ patterns.forEach(url -> {
+ excludes.add(ReUtil.replaceAll(url, PATTERN, asterisk));
+ });
+ });
+
+ // 获取类上边的注解, 替代path variable 为 *
+ Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
+ Optional.ofNullable(controller).ifPresent(anonymous -> {
+ Set patterns = info.getPatternsCondition().getPatterns();
+ patterns.forEach(url -> {
+ excludes.add(ReUtil.replaceAll(url, PATTERN, asterisk));
+ });
+ });
+ });
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/config/properties/MailProperties.java b/ruoyi/src/main/java/com/ruoyi/framework/config/properties/MailProperties.java
new file mode 100644
index 000000000..95e6cb8ba
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/framework/config/properties/MailProperties.java
@@ -0,0 +1,71 @@
+package com.ruoyi.framework.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * JavaMail 配置属性
+ *
+ * @author Michelle.Chung
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "mail")
+public class MailProperties {
+
+ /**
+ * 过滤开关
+ */
+ private Boolean enabled;
+
+ /**
+ * SMTP服务器域名
+ */
+ private String host;
+
+ /**
+ * SMTP服务端口
+ */
+ private Integer port;
+
+ /**
+ * 是否需要用户名密码验证
+ */
+ private Boolean auth;
+
+ /**
+ * 用户名
+ */
+ private String user;
+
+ /**
+ * 密码
+ */
+ private String pass;
+
+ /**
+ * 发送方,遵循RFC-822标准
+ */
+ private String from;
+
+ /**
+ * 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接(TLS或SSL), 而不是使用一个单独的加密通信端口。
+ */
+ private Boolean starttlsEnable;
+
+ /**
+ * 使用 SSL安全连接
+ */
+ private Boolean sslEnable;
+
+ /**
+ * SMTP超时时长,单位毫秒,缺省值不超时
+ */
+ private Long timeout;
+
+ /**
+ * Socket连接超时值,单位毫秒,缺省值不超时
+ */
+ private Long connectionTimeout;
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/listener/UserActionListener.java b/ruoyi/src/main/java/com/ruoyi/framework/listener/UserActionListener.java
index 75277c257..dcb9b1996 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/listener/UserActionListener.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/listener/UserActionListener.java
@@ -3,7 +3,6 @@ package com.ruoyi.framework.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
-import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.ruoyi.common.constant.Constants;
@@ -18,7 +17,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
/**
* 用户行为 侦听器的实现
@@ -36,13 +35,12 @@ public class UserActionListener implements SaTokenListener {
* 每次登录时触发
*/
@Override
- public void doLogin(String loginType, Object loginId, SaLoginModel loginModel) {
+ public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
UserType userType = UserType.getUserType(loginId.toString());
if (userType == UserType.SYS_USER) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
LoginUser user = LoginHelper.getLoginUser();
- String tokenValue = StpUtil.getTokenValueByLoginId(loginId);
UserOnlineDTO dto = new UserOnlineDTO();
dto.setIpaddr(ip);
dto.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
@@ -52,7 +50,7 @@ public class UserActionListener implements SaTokenListener {
dto.setTokenId(tokenValue);
dto.setUserName(user.getUsername());
dto.setDeptName(user.getDeptName());
- RedisUtils.setCacheObject(Constants.ONLINE_TOKEN_KEY + tokenValue, dto, tokenConfig.getTimeout(), TimeUnit.SECONDS);
+ RedisUtils.setCacheObject(Constants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
} else if (userType == UserType.APP_USER) {
// app端 自行根据业务编写
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/satoken/dao/PlusSaTokenDao.java b/ruoyi/src/main/java/com/ruoyi/framework/satoken/dao/PlusSaTokenDao.java
index f78e81415..68df5a646 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/satoken/dao/PlusSaTokenDao.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/satoken/dao/PlusSaTokenDao.java
@@ -5,10 +5,10 @@ import cn.dev33.satoken.util.SaFoxUtil;
import com.ruoyi.common.utils.redis.RedisUtils;
import org.springframework.stereotype.Component;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-import java.util.concurrent.TimeUnit;
/**
* Sa-Token持久层接口(使用框架自带RedisUtils实现 协议统一)
@@ -38,7 +38,7 @@ public class PlusSaTokenDao implements SaTokenDao {
if (timeout == SaTokenDao.NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, value);
} else {
- RedisUtils.setCacheObject(key, value, timeout, TimeUnit.SECONDS);
+ RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
}
@@ -68,7 +68,8 @@ public class PlusSaTokenDao implements SaTokenDao {
*/
@Override
public long getTimeout(String key) {
- return RedisUtils.getTimeToLive(key) / 1000;
+ long timeout = RedisUtils.getTimeToLive(key);
+ return timeout < 0 ? timeout : timeout / 1000;
}
/**
@@ -87,7 +88,7 @@ public class PlusSaTokenDao implements SaTokenDao {
}
return;
}
- RedisUtils.expire(key, timeout, TimeUnit.SECONDS);
+ RedisUtils.expire(key, Duration.ofSeconds(timeout));
}
@@ -111,7 +112,7 @@ public class PlusSaTokenDao implements SaTokenDao {
if (timeout == SaTokenDao.NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, object);
} else {
- RedisUtils.setCacheObject(key, object, timeout, TimeUnit.SECONDS);
+ RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
}
@@ -141,7 +142,8 @@ public class PlusSaTokenDao implements SaTokenDao {
*/
@Override
public long getObjectTimeout(String key) {
- return RedisUtils.getTimeToLive(key) / 1000;
+ long timeout = RedisUtils.getTimeToLive(key);
+ return timeout < 0 ? timeout : timeout / 1000;
}
/**
@@ -160,7 +162,7 @@ public class PlusSaTokenDao implements SaTokenDao {
}
return;
}
- RedisUtils.expire(key, timeout, TimeUnit.SECONDS);
+ RedisUtils.expire(key, Duration.ofSeconds(timeout));
}
diff --git a/ruoyi/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
index 9dccab2d1..2a99a2e65 100644
--- a/ruoyi/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/generator/service/GenTableServiceImpl.java
@@ -33,6 +33,7 @@ import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -135,6 +136,7 @@ public class GenTableServiceImpl implements IGenTableService {
* @param genTable 业务信息
* @return 结果
*/
+ @Transactional(rollbackFor = Exception.class)
@Override
public void updateGenTable(GenTable genTable) {
String options = JsonUtils.toJsonString(genTable.getParams());
@@ -153,6 +155,7 @@ public class GenTableServiceImpl implements IGenTableService {
* @param tableIds 需要删除的数据ID
* @return 结果
*/
+ @Transactional(rollbackFor = Exception.class)
@Override
public void deleteGenTableByIds(Long[] tableIds) {
List ids = Arrays.asList(tableIds);
@@ -165,6 +168,7 @@ public class GenTableServiceImpl implements IGenTableService {
*
* @param tableList 导入表列表
*/
+ @Transactional(rollbackFor = Exception.class)
@Override
public void importGenTable(List tableList) {
String operName = LoginHelper.getUsername();
@@ -284,6 +288,7 @@ public class GenTableServiceImpl implements IGenTableService {
*
* @param tableName 表名称
*/
+ @Transactional(rollbackFor = Exception.class)
@Override
public void synchDb(String tableName) {
GenTable table = baseMapper.selectGenTableByName(tableName);
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/constant/OssConstant.java b/ruoyi/src/main/java/com/ruoyi/oss/constant/OssConstant.java
index 6c00cd8db..1d1a77708 100644
--- a/ruoyi/src/main/java/com/ruoyi/oss/constant/OssConstant.java
+++ b/ruoyi/src/main/java/com/ruoyi/oss/constant/OssConstant.java
@@ -35,6 +35,11 @@ public interface OssConstant {
*/
List SYSTEM_DATA_IDS = Arrays.asList(1, 2, 3, 4);
+ /**
+ * 云服务商
+ */
+ String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu"};
+
/**
* https 状态
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java b/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java
new file mode 100644
index 000000000..6f6be8f8c
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java
@@ -0,0 +1,188 @@
+package com.ruoyi.oss.core;
+
+import cn.hutool.core.util.IdUtil;
+import com.amazonaws.ClientConfiguration;
+import com.amazonaws.Protocol;
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSCredentialsProvider;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.services.s3.model.CannedAccessControlList;
+import com.amazonaws.services.s3.model.CreateBucketRequest;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import com.amazonaws.services.s3.model.PutObjectRequest;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.oss.constant.OssConstant;
+import com.ruoyi.oss.entity.UploadResult;
+import com.ruoyi.oss.enumd.PolicyType;
+import com.ruoyi.oss.exception.OssException;
+import com.ruoyi.oss.properties.OssProperties;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * S3 存储协议 所有兼容S3协议的云厂商均支持
+ * 阿里云 腾讯云 七牛云 minio
+ *
+ * @author Lion Li
+ */
+public class OssClient {
+
+ private final String configKey;
+
+ private final OssProperties properties;
+
+ private final AmazonS3 client;
+
+ public OssClient(String configKey, OssProperties ossProperties) {
+ this.configKey = configKey;
+ this.properties = ossProperties;
+ try {
+ AwsClientBuilder.EndpointConfiguration endpointConfig =
+ new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
+
+ AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
+ AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
+ ClientConfiguration clientConfig = new ClientConfiguration();
+ if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
+ clientConfig.setProtocol(Protocol.HTTPS);
+ } else {
+ clientConfig.setProtocol(Protocol.HTTP);
+ }
+ this.client = AmazonS3Client.builder()
+ .withEndpointConfiguration(endpointConfig)
+ .withClientConfiguration(clientConfig)
+ .withCredentials(credentialsProvider)
+ .disableChunkedEncoding()
+ .build();
+
+ createBucket();
+ } catch (Exception e) {
+ if (e instanceof OssException) {
+ throw e;
+ }
+ throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
+ }
+ }
+
+ public void createBucket() {
+ try {
+ String bucketName = properties.getBucketName();
+ if (client.doesBucketExistV2(bucketName)) {
+ return;
+ }
+ CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
+ createBucketRequest.setCannedAcl(CannedAccessControlList.PublicRead);
+ client.createBucket(createBucketRequest);
+ client.setBucketPolicy(bucketName, getPolicy(bucketName, PolicyType.READ));
+ } catch (Exception e) {
+ throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
+ }
+ }
+
+ public UploadResult upload(byte[] data, String path, String contentType) {
+ return upload(new ByteArrayInputStream(data), path, contentType);
+ }
+
+ public UploadResult upload(InputStream inputStream, String path, String contentType) {
+ try {
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentType(contentType);
+ metadata.setContentLength(inputStream.available());
+ client.putObject(new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata));
+ } 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 {
+ client.deleteObject(properties.getBucketName(), path);
+ } catch (Exception e) {
+ throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+ }
+ }
+
+ public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
+ return upload(data, getPath(properties.getPrefix(), suffix), contentType);
+ }
+
+ public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
+ return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
+ }
+
+ public String getUrl() {
+ String domain = properties.getDomain();
+ if (StringUtils.isNotBlank(domain)) {
+ return domain;
+ }
+ String endpoint = properties.getEndpoint();
+ String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
+ // 云服务商直接返回
+ if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)){
+ return header + properties.getBucketName() + "." + endpoint;
+ }
+ // minio 单独处理
+ return header + endpoint + "/" + properties.getBucketName();
+ }
+
+ public String getPath(String prefix, String suffix) {
+ // 生成uuid
+ String uuid = IdUtil.fastSimpleUUID();
+ // 文件路径
+ String path = DateUtils.datePath() + "/" + uuid;
+ if (StringUtils.isNotBlank(prefix)) {
+ path = prefix + "/" + path;
+ }
+ return path + suffix;
+ }
+
+
+ public String getConfigKey() {
+ return configKey;
+ }
+
+ private static String getPolicy(String bucketName, PolicyType policyType) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
+ if (policyType == PolicyType.WRITE) {
+ builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n");
+ } else if (policyType == PolicyType.READ_WRITE) {
+ builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n");
+ } else {
+ builder.append("\"s3:GetBucketLocation\"\n");
+ }
+ builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("\"\n},\n");
+ if (policyType == PolicyType.READ) {
+ builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("\"\n},\n");
+ }
+ builder.append("{\n\"Action\": ");
+ switch (policyType) {
+ case WRITE:
+ builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
+ break;
+ case READ_WRITE:
+ builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
+ break;
+ default:
+ builder.append("\"s3:GetObject\",\n");
+ break;
+ }
+ builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
+ builder.append(bucketName);
+ builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
+ return builder.toString();
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/enumd/OssEnumd.java b/ruoyi/src/main/java/com/ruoyi/oss/enumd/OssEnumd.java
deleted file mode 100644
index e16a67338..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/enumd/OssEnumd.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.ruoyi.oss.enumd;
-
-import com.ruoyi.oss.service.impl.AliyunOssStrategy;
-import com.ruoyi.oss.service.impl.MinioOssStrategy;
-import com.ruoyi.oss.service.impl.QcloudOssStrategy;
-import com.ruoyi.oss.service.impl.QiniuOssStrategy;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * 对象存储服务商枚举
- *
- * @author Lion Li
- */
-@Getter
-@AllArgsConstructor
-public enum OssEnumd {
-
- /**
- * 七牛云
- */
- QINIU("qiniu", QiniuOssStrategy.class),
-
- /**
- * 阿里云
- */
- ALIYUN("aliyun", AliyunOssStrategy.class),
-
- /**
- * 腾讯云
- */
- QCLOUD("qcloud", QcloudOssStrategy.class),
-
- /**
- * minio
- */
- MINIO("minio", MinioOssStrategy.class);
-
- private final String value;
-
- private final Class> beanClass;
-
- public static OssEnumd find(String value) {
- for (OssEnumd enumd : values()) {
- if (enumd.getValue().equals(value)) {
- return enumd;
- }
- }
- return null;
- }
-
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/enumd/PolicyType.java b/ruoyi/src/main/java/com/ruoyi/oss/enumd/PolicyType.java
index e1925dcfb..606f0f484 100644
--- a/ruoyi/src/main/java/com/ruoyi/oss/enumd/PolicyType.java
+++ b/ruoyi/src/main/java/com/ruoyi/oss/enumd/PolicyType.java
@@ -1,19 +1,3 @@
-/*
- * Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * Neither the name of the dreamlu.net developer nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- * Author: Chill 庄骞 (smallchill@163.com)
- */
package com.ruoyi.oss.enumd;
import lombok.AllArgsConstructor;
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/factory/OssFactory.java b/ruoyi/src/main/java/com/ruoyi/oss/factory/OssFactory.java
index 9ac887e65..7065c4a4a 100644
--- a/ruoyi/src/main/java/com/ruoyi/oss/factory/OssFactory.java
+++ b/ruoyi/src/main/java/com/ruoyi/oss/factory/OssFactory.java
@@ -3,15 +3,15 @@ package com.ruoyi.oss.factory;
import com.ruoyi.common.utils.JsonUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.redis.RedisUtils;
-import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.oss.constant.OssConstant;
-import com.ruoyi.oss.enumd.OssEnumd;
+import com.ruoyi.oss.core.OssClient;
import com.ruoyi.oss.exception.OssException;
import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.IOssStrategy;
-import com.ruoyi.oss.service.abstractd.AbstractOssStrategy;
import lombok.extern.slf4j.Slf4j;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
/**
* 文件上传Factory
*
@@ -20,17 +20,19 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class OssFactory {
+ private static final Map CLIENT_CACHE = new ConcurrentHashMap<>();
+
/**
* 初始化工厂
*/
public static void init() {
log.info("初始化OSS工厂");
- RedisUtils.subscribe(OssConstant.CACHE_CONFIG_KEY, String.class, type -> {
- AbstractOssStrategy strategy = getStrategy(type);
+ RedisUtils.subscribe(OssConstant.CACHE_CONFIG_KEY, String.class, configKey -> {
+ OssClient client = getClient(configKey);
// 未初始化不处理
- if (strategy.isInit) {
- refresh(type);
- log.info("订阅刷新OSS配置 => " + type);
+ if (client != null) {
+ refresh(configKey);
+ log.info("订阅刷新OSS配置 => " + configKey);
}
});
}
@@ -38,42 +40,38 @@ public class OssFactory {
/**
* 获取默认实例
*/
- public static IOssStrategy instance() {
+ public static OssClient instance() {
// 获取redis 默认类型
- String type = RedisUtils.getCacheObject(OssConstant.CACHE_CONFIG_KEY);
- if (StringUtils.isEmpty(type)) {
+ String configKey = RedisUtils.getCacheObject(OssConstant.CACHE_CONFIG_KEY);
+ if (StringUtils.isEmpty(configKey)) {
throw new OssException("文件存储服务类型无法找到!");
}
- return instance(type);
+ return instance(configKey);
}
/**
* 根据类型获取实例
*/
- public static IOssStrategy instance(String type) {
- OssEnumd enumd = OssEnumd.find(type);
- if (enumd == null) {
- throw new OssException("文件存储服务类型无法找到!");
+ public static OssClient instance(String configKey) {
+ OssClient client = getClient(configKey);
+ if (client == null) {
+ refresh(configKey);
+ return getClient(configKey);
}
- AbstractOssStrategy strategy = getStrategy(type);
- if (!strategy.isInit) {
- refresh(type);
- }
- return strategy;
+ return client;
}
- private static void refresh(String type) {
- Object json = RedisUtils.getCacheObject(OssConstant.SYS_OSS_KEY + type);
+ private static void refresh(String configKey) {
+ Object json = RedisUtils.getCacheObject(OssConstant.SYS_OSS_KEY + configKey);
OssProperties properties = JsonUtils.parseObject(json.toString(), OssProperties.class);
if (properties == null) {
- throw new OssException("系统异常, '" + type + "'配置信息不存在!");
+ throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
}
- getStrategy(type).init(properties);
+ CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
}
- private static AbstractOssStrategy getStrategy(String type) {
- OssEnumd enumd = OssEnumd.find(type);
- return (AbstractOssStrategy) SpringUtils.getBean(enumd.getBeanClass());
+ private static OssClient getClient(String configKey) {
+ return CLIENT_CACHE.get(configKey);
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/properties/OssProperties.java b/ruoyi/src/main/java/com/ruoyi/oss/properties/OssProperties.java
index d09bfdb69..a01777901 100644
--- a/ruoyi/src/main/java/com/ruoyi/oss/properties/OssProperties.java
+++ b/ruoyi/src/main/java/com/ruoyi/oss/properties/OssProperties.java
@@ -11,10 +11,15 @@ import lombok.Data;
public class OssProperties {
/**
- * 域名
+ * 访问站点
*/
private String endpoint;
+ /**
+ * 自定义域名
+ */
+ private String domain;
+
/**
* 前缀
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/IOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/IOssStrategy.java
deleted file mode 100644
index 981c23e9e..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/IOssStrategy.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.ruoyi.oss.service;
-
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-
-import java.io.InputStream;
-
-/**
- * 对象存储策略
- *
- * @author Lion Li
- */
-public interface IOssStrategy {
-
- /**
- * 创建存储桶
- */
- void createBucket();
-
- /**
- * 获取服务商类型
- * @return 对象存储服务商枚举
- */
- OssEnumd getServiceType();
-
- /**
- * 文件上传
- *
- * @param data 文件字节数组
- * @param path 文件路径,包含文件名
- * @param contentType 文件类型
- * @return 返回http地址
- */
- UploadResult upload(byte[] data, String path, String contentType);
-
- /**
- * 文件删除
- *
- * @param path 文件路径,包含文件名
- */
- void delete(String path);
-
- /**
- * 文件上传
- *
- * @param data 文件字节数组
- * @param suffix 后缀
- * @param contentType 文件类型
- * @return 返回http地址
- */
- UploadResult uploadSuffix(byte[] data, String suffix, String contentType);
-
- /**
- * 文件上传
- *
- * @param inputStream 字节流
- * @param path 文件路径,包含文件名
- * @param contentType 文件类型
- * @return 返回http地址
- */
- UploadResult upload(InputStream inputStream, String path, String contentType);
-
- /**
- * 文件上传
- *
- * @param inputStream 字节流
- * @param suffix 后缀
- * @param contentType 文件类型
- * @return 返回http地址
- */
- UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType);
-
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/abstractd/AbstractOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/abstractd/AbstractOssStrategy.java
deleted file mode 100644
index ae17c19de..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/abstractd/AbstractOssStrategy.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.ruoyi.oss.service.abstractd;
-
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.IdUtil;
-import com.ruoyi.common.utils.DateUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.IOssStrategy;
-
-import java.io.InputStream;
-
-/**
- * 对象存储策略(支持七牛、阿里云、腾讯云、minio)
- *
- * @author Lion Li
- */
-public abstract class AbstractOssStrategy implements IOssStrategy {
-
- protected OssProperties properties;
- public boolean isInit = false;
-
- public void init(OssProperties properties) {
- this.properties = properties;
- }
-
- @Override
- public abstract void createBucket();
-
- @Override
- public abstract OssEnumd getServiceType();
-
- public String getPath(String prefix, String suffix) {
- // 生成uuid
- String uuid = IdUtil.fastSimpleUUID();
- // 文件路径
- String path = DateUtils.datePath() + "/" + uuid;
- if (StringUtils.isNotBlank(prefix)) {
- path = prefix + "/" + path;
- }
- return path + suffix;
- }
-
- @Override
- public abstract UploadResult upload(byte[] data, String path, String contentType);
-
- @Override
- public abstract void delete(String path);
-
- @Override
- public UploadResult upload(InputStream inputStream, String path, String contentType) {
- byte[] data = IoUtil.readBytes(inputStream);
- return this.upload(data, path, contentType);
- }
-
- @Override
- public abstract UploadResult uploadSuffix(byte[] data, String suffix, String contentType);
-
- @Override
- public abstract UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType);
-
- /**
- * 获取域名访问链接
- *
- * @return 域名访问链接
- */
- public abstract String getEndpointLink();
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/AliyunOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/impl/AliyunOssStrategy.java
deleted file mode 100644
index 62c29228f..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/AliyunOssStrategy.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.ruoyi.oss.service.impl;
-
-import com.aliyun.oss.ClientConfiguration;
-import com.aliyun.oss.OSSClient;
-import com.aliyun.oss.common.auth.DefaultCredentialProvider;
-import com.aliyun.oss.common.comm.Protocol;
-import com.aliyun.oss.model.CannedAccessControlList;
-import com.aliyun.oss.model.CreateBucketRequest;
-import com.aliyun.oss.model.ObjectMetadata;
-import com.aliyun.oss.model.PutObjectRequest;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.oss.constant.OssConstant;
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-import com.ruoyi.oss.exception.OssException;
-import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.abstractd.AbstractOssStrategy;
-import org.springframework.stereotype.Component;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * 阿里云存储策略
- *
- * @author Lion Li
- */
-@Component
-public class AliyunOssStrategy extends AbstractOssStrategy {
-
- private OSSClient client;
-
- @Override
- public void init(OssProperties ossProperties) {
- super.init(ossProperties);
- try {
- ClientConfiguration configuration = new ClientConfiguration();
- if (OssConstant.IS_HTTPS.equals(ossProperties.getIsHttps())) {
- configuration.setProtocol(Protocol.HTTPS);
- }
- DefaultCredentialProvider credentialProvider = new DefaultCredentialProvider(
- properties.getAccessKey(), properties.getSecretKey());
- client = new OSSClient(properties.getEndpoint(), credentialProvider, configuration);
- createBucket();
- } catch (Exception e) {
- throw new OssException("阿里云存储配置错误! 请检查系统配置:[" + e.getMessage() + "]");
- }
- isInit = true;
- }
-
- @Override
- public void createBucket() {
- try {
- String bucketName = properties.getBucketName();
- if (client.doesBucketExist(bucketName)) {
- return;
- }
- CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
- createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
- client.createBucket(createBucketRequest);
- } catch (Exception e) {
- throw new OssException("创建Bucket失败, 请核对阿里云配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public OssEnumd getServiceType() {
- return OssEnumd.ALIYUN;
- }
-
- @Override
- public UploadResult upload(byte[] data, String path, String contentType) {
- return upload(new ByteArrayInputStream(data), path, contentType);
- }
-
- @Override
- public UploadResult upload(InputStream inputStream, String path, String contentType) {
- try {
- ObjectMetadata metadata = new ObjectMetadata();
- metadata.setContentType(contentType);
- client.putObject(new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata));
- } catch (Exception e) {
- throw new OssException("上传文件失败,请检查阿里云配置信息:[" + e.getMessage() + "]");
- }
- return UploadResult.builder().url(getEndpointLink() + "/" + path).filename(path).build(); }
-
- @Override
- public void delete(String path) {
- path = path.replace(getEndpointLink() + "/", "");
- try {
- client.deleteObject(properties.getBucketName(), path);
- } catch (Exception e) {
- throw new OssException("上传文件失败,请检查阿里云配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
- return upload(data, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
- return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public String getEndpointLink() {
- String endpoint = properties.getEndpoint();
- StringBuilder sb = new StringBuilder(endpoint);
- if (StringUtils.containsAnyIgnoreCase(endpoint, "http://")) {
- sb.insert(7, properties.getBucketName() + ".");
- } else if (StringUtils.containsAnyIgnoreCase(endpoint, "https://")) {
- sb.insert(8, properties.getBucketName() + ".");
- } else {
- throw new OssException("Endpoint配置错误");
- }
- return sb.toString();
- }
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/MinioOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/impl/MinioOssStrategy.java
deleted file mode 100644
index f5be957fa..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/MinioOssStrategy.java
+++ /dev/null
@@ -1,194 +0,0 @@
-package com.ruoyi.oss.service.impl;
-
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.oss.constant.OssConstant;
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-import com.ruoyi.oss.enumd.PolicyType;
-import com.ruoyi.oss.exception.OssException;
-import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.abstractd.AbstractOssStrategy;
-import io.minio.*;
-import io.minio.http.HttpUtils;
-import okhttp3.HttpUrl;
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Component;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * minio存储策略
- *
- * @author Lion Li
- */
-@Component
-public class MinioOssStrategy extends AbstractOssStrategy {
-
- private MinioClient minioClient;
-
- @Override
- public void init(OssProperties ossProperties) {
- super.init(ossProperties);
- try {
- MinioClient.Builder builder = MinioClient.builder();
- if (OssConstant.IS_HTTPS.equals(ossProperties.getIsHttps())) {
- HttpUrl url = HttpUtils.getBaseUrl(properties.getEndpoint())
- .newBuilder().scheme("https").build();
- builder.endpoint(url);
- } else {
- builder.endpoint(properties.getEndpoint());
- }
- minioClient = builder.credentials(properties.getAccessKey(), properties.getSecretKey()).build();
- createBucket();
- } catch (Exception e) {
- throw new OssException("Minio存储配置错误! 请检查系统配置:[" + e.getMessage() + "]");
- }
- isInit = true;
- }
-
- @Override
- public void createBucket() {
- try {
- String bucketName = properties.getBucketName();
- boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
- if (exists) {
- return;
- }
- // 不存在就创建桶
- minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
- minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
- .bucket(bucketName)
- .config(getPolicy(bucketName, PolicyType.READ))
- .build());
- } catch (Exception e) {
- throw new OssException("创建Bucket失败, 请核对Minio配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public OssEnumd getServiceType() {
- return OssEnumd.MINIO;
- }
-
- @Override
- public UploadResult upload(byte[] data, String path, String contentType) {
- return upload(new ByteArrayInputStream(data), path, contentType);
- }
-
- @Override
- public UploadResult upload(InputStream inputStream, String path, String contentType) {
- try {
- // 解决 inputStream.available() 再 socket 下传输延迟问题 导致获取数值不精确
- Thread.sleep(1000);
- minioClient.putObject(PutObjectArgs.builder()
- .bucket(properties.getBucketName())
- .object(path)
- .contentType(StringUtils.blankToDefault(contentType, MediaType.APPLICATION_OCTET_STREAM_VALUE))
- .stream(inputStream, inputStream.available(), -1)
- .build());
- } catch (Exception e) {
- throw new OssException("上传文件失败,请核对Minio配置信息:[" + e.getMessage() + "]");
- }
- return UploadResult.builder().url(getEndpointLink() + "/" + path).filename(path).build();
- }
-
- @Override
- public void delete(String path) {
- path = path.replace(getEndpointLink() + "/", "");
- try {
- minioClient.removeObject(RemoveObjectArgs.builder()
- .bucket(properties.getBucketName())
- .object(path)
- .build());
- } catch (Exception e) {
- throw new OssException(e.getMessage());
- }
- }
-
- @Override
- public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
- return upload(data, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
- return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public String getEndpointLink() {
- return properties.getEndpoint() + "/" + properties.getBucketName();
- }
-
- private String getPolicy(String bucketName, PolicyType policyType) {
- StringBuilder builder = new StringBuilder();
- builder.append("{\n");
- builder.append(" \"Statement\": [\n");
- builder.append(" {\n");
- builder.append(" \"Action\": [\n");
- if (policyType == PolicyType.WRITE) {
- builder.append(" \"s3:GetBucketLocation\",\n");
- builder.append(" \"s3:ListBucketMultipartUploads\"\n");
- } else if (policyType == PolicyType.READ_WRITE) {
- builder.append(" \"s3:GetBucketLocation\",\n");
- builder.append(" \"s3:ListBucket\",\n");
- builder.append(" \"s3:ListBucketMultipartUploads\"\n");
- } else {
- builder.append(" \"s3:GetBucketLocation\"\n");
- }
- builder.append(" ],\n");
- builder.append(" \"Effect\": \"Allow\",\n");
- builder.append(" \"Principal\": \"*\",\n");
- builder.append(" \"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("\"\n");
- builder.append(" },\n");
- if (PolicyType.READ.equals(policyType)) {
- builder.append(" {\n");
- builder.append(" \"Action\": [\n");
- builder.append(" \"s3:ListBucket\"\n");
- builder.append(" ],\n");
- builder.append(" \"Effect\": \"Deny\",\n");
- builder.append(" \"Principal\": \"*\",\n");
- builder.append(" \"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("\"\n");
- builder.append(" },\n");
- }
- builder.append(" {\n");
- builder.append(" \"Action\": ");
- switch (policyType) {
- case WRITE:
- builder.append("[\n");
- builder.append(" \"s3:AbortMultipartUpload\",\n");
- builder.append(" \"s3:DeleteObject\",\n");
- builder.append(" \"s3:ListMultipartUploadParts\",\n");
- builder.append(" \"s3:PutObject\"\n");
- builder.append(" ],\n");
- break;
- case READ_WRITE:
- builder.append("[\n");
- builder.append(" \"s3:AbortMultipartUpload\",\n");
- builder.append(" \"s3:DeleteObject\",\n");
- builder.append(" \"s3:GetObject\",\n");
- builder.append(" \"s3:ListMultipartUploadParts\",\n");
- builder.append(" \"s3:PutObject\"\n");
- builder.append(" ],\n");
- break;
- default:
- builder.append("\"s3:GetObject\",\n");
- break;
- }
- builder.append(" \"Effect\": \"Allow\",\n");
- builder.append(" \"Principal\": \"*\",\n");
- builder.append(" \"Resource\": \"arn:aws:s3:::");
- builder.append(bucketName);
- builder.append("/*\"\n");
- builder.append(" }\n");
- builder.append(" ],\n");
- builder.append(" \"Version\": \"2012-10-17\"\n");
- builder.append("}\n");
- return builder.toString();
- }
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QcloudOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QcloudOssStrategy.java
deleted file mode 100644
index 756462e4d..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QcloudOssStrategy.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package com.ruoyi.oss.service.impl;
-
-import com.qcloud.cos.COSClient;
-import com.qcloud.cos.ClientConfig;
-import com.qcloud.cos.auth.BasicCOSCredentials;
-import com.qcloud.cos.auth.COSCredentials;
-import com.qcloud.cos.http.HttpProtocol;
-import com.qcloud.cos.model.*;
-import com.qcloud.cos.region.Region;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.oss.constant.OssConstant;
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-import com.ruoyi.oss.exception.OssException;
-import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.abstractd.AbstractOssStrategy;
-import org.springframework.stereotype.Component;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * 腾讯云存储策略
- *
- * @author Lion Li
- */
-@Component
-public class QcloudOssStrategy extends AbstractOssStrategy {
-
- private COSClient client;
-
- @Override
- public void init(OssProperties ossProperties) {
- super.init(ossProperties);
- try {
- COSCredentials credentials = new BasicCOSCredentials(
- properties.getAccessKey(), properties.getSecretKey());
- // 初始化客户端配置
- ClientConfig clientConfig = new ClientConfig();
- // 设置bucket所在的区域,华南:gz 华北:tj 华东:sh
- clientConfig.setRegion(new Region(properties.getRegion()));
- if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
- clientConfig.setHttpProtocol(HttpProtocol.https);
- } else {
- clientConfig.setHttpProtocol(HttpProtocol.http);
- }
- client = new COSClient(credentials, clientConfig);
- createBucket();
- } catch (Exception e) {
- throw new OssException("腾讯云存储配置错误! 请检查系统配置:[" + e.getMessage() + "]");
- }
- isInit = true;
- }
-
- @Override
- public void createBucket() {
- try {
- String bucketName = properties.getBucketName();
- if (client.doesBucketExist(bucketName)) {
- return;
- }
- CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
- createBucketRequest.setCannedAcl(CannedAccessControlList.PublicRead);
- client.createBucket(createBucketRequest);
- } catch (Exception e) {
- throw new OssException("创建Bucket失败, 请核对腾讯云配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public OssEnumd getServiceType() {
- return OssEnumd.QCLOUD;
- }
-
- @Override
- public UploadResult upload(byte[] data, String path, String contentType) {
- return upload(new ByteArrayInputStream(data), path, contentType);
- }
-
- @Override
- public UploadResult upload(InputStream inputStream, String path, String contentType) {
- try {
- ObjectMetadata metadata = new ObjectMetadata();
- metadata.setContentType(contentType);
- client.putObject(new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata));
- } catch (Exception e) {
- throw new OssException("上传文件失败,请检查腾讯云配置信息:[" + e.getMessage() + "]");
- }
- return UploadResult.builder().url(getEndpointLink() + "/" + path).filename(path).build();
- }
-
- @Override
- public void delete(String path) {
- path = path.replace(getEndpointLink() + "/", "");
- try {
- client.deleteObject(new DeleteObjectRequest(properties.getBucketName(), path));
- } catch (Exception e) {
- throw new OssException("上传文件失败,请检腾讯云查配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
- return upload(data, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
- return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public String getEndpointLink() {
- String endpoint = properties.getEndpoint();
- StringBuilder sb = new StringBuilder(endpoint);
- if (StringUtils.containsAnyIgnoreCase(endpoint, "http://")) {
- sb.insert(7, properties.getBucketName() + ".");
- } else if (StringUtils.containsAnyIgnoreCase(endpoint, "https://")) {
- sb.insert(8, properties.getBucketName() + ".");
- } else {
- throw new OssException("Endpoint配置错误");
- }
- return sb.toString();
- }
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QiniuOssStrategy.java b/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QiniuOssStrategy.java
deleted file mode 100644
index 20f13eca1..000000000
--- a/ruoyi/src/main/java/com/ruoyi/oss/service/impl/QiniuOssStrategy.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.ruoyi.oss.service.impl;
-
-import cn.hutool.core.util.ArrayUtil;
-import com.qiniu.http.Response;
-import com.qiniu.storage.BucketManager;
-import com.qiniu.storage.Configuration;
-import com.qiniu.storage.Region;
-import com.qiniu.storage.UploadManager;
-import com.qiniu.util.Auth;
-import com.ruoyi.oss.constant.OssConstant;
-import com.ruoyi.oss.entity.UploadResult;
-import com.ruoyi.oss.enumd.OssEnumd;
-import com.ruoyi.oss.exception.OssException;
-import com.ruoyi.oss.properties.OssProperties;
-import com.ruoyi.oss.service.abstractd.AbstractOssStrategy;
-import org.springframework.stereotype.Component;
-
-import java.io.InputStream;
-
-/**
- * 七牛云存储策略
- *
- * @author Lion Li
- */
-@Component
-public class QiniuOssStrategy extends AbstractOssStrategy {
-
- private UploadManager uploadManager;
- private BucketManager bucketManager;
- private Auth auth;
-
-
- @Override
- public void init(OssProperties ossProperties) {
- super.init(ossProperties);
- try {
- Configuration config = new Configuration(getRegion(properties.getRegion()));
- // https设置
- config.useHttpsDomains = OssConstant.IS_HTTPS.equals(properties.getIsHttps());
- uploadManager = new UploadManager(config);
- auth = Auth.create(properties.getAccessKey(), properties.getSecretKey());
- bucketManager = new BucketManager(auth, config);
- createBucket();
- } catch (Exception e) {
- throw new OssException("七牛云存储配置错误! 请检查系统配置:[" + e.getMessage() + "]");
- }
- isInit = true;
- }
-
- @Override
- public void createBucket() {
- try {
- String bucketName = properties.getBucketName();
- if (ArrayUtil.contains(bucketManager.buckets(), bucketName)) {
- return;
- }
- bucketManager.createBucket(bucketName, properties.getRegion());
- } catch (Exception e) {
- throw new OssException("创建Bucket失败, 请核对七牛云配置信息:[" + e.getMessage() + "]");
- }
- }
-
- @Override
- public OssEnumd getServiceType() {
- return OssEnumd.QINIU;
- }
-
- @Override
- public UploadResult upload(byte[] data, String path, String contentType) {
- try {
- String token = auth.uploadToken(properties.getBucketName());
- Response res = uploadManager.put(data, path, token, null, contentType, false);
- if (!res.isOK()) {
- throw new RuntimeException("上传七牛出错:" + res.error);
- }
- } catch (Exception e) {
- throw new OssException("上传文件失败,请核对七牛配置信息:[" + e.getMessage() + "]");
- }
- return UploadResult.builder().url(getEndpointLink() + "/" + path).filename(path).build();
- }
-
- @Override
- public void delete(String path) {
- try {
- path = path.replace(getEndpointLink() + "/", "");
- Response res = bucketManager.delete(properties.getBucketName(), path);
- if (!res.isOK()) {
- throw new RuntimeException("删除七牛文件出错:" + res.error);
- }
- } catch (Exception e) {
- throw new OssException(e.getMessage());
- }
- }
-
- @Override
- public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
- return upload(data, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
- return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
- }
-
- @Override
- public String getEndpointLink() {
- return properties.getEndpoint();
- }
-
- private Region getRegion(String region) {
- switch (region) {
- case "z0":
- return Region.region0();
- case "z1":
- return Region.region1();
- case "z2":
- return Region.region2();
- case "na0":
- return Region.regionNa0();
- case "as0":
- return Region.regionAs0();
- default:
- return Region.autoRegion();
- }
- }
-
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java b/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java
new file mode 100644
index 000000000..e9ce2b4de
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java
@@ -0,0 +1,46 @@
+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
+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);
+ }
+
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java b/ruoyi/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
new file mode 100644
index 000000000..39359cdfd
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
@@ -0,0 +1,47 @@
+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;
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java b/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
new file mode 100644
index 000000000..eede376e5
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
@@ -0,0 +1,66 @@
+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 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(resp)
+ .build();
+ } catch (Exception e) {
+ throw new SmsException(e.getMessage());
+ }
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java b/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
new file mode 100644
index 000000000..0aec3ddbe
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
@@ -0,0 +1,26 @@
+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 param);
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java b/ruoyi/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
new file mode 100644
index 000000000..1de8eae14
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
@@ -0,0 +1,81 @@
+package com.ruoyi.sms.core;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+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 param) {
+ if (StringUtils.isBlank(phones)) {
+ throw new SmsException("手机号不能为空");
+ }
+ if (StringUtils.isBlank(templateId)) {
+ throw new SmsException("模板ID不能为空");
+ }
+ SendSmsRequest req = new SendSmsRequest();
+ Set set = Arrays.stream(phones.split(",")).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(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());
+ }
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java b/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java
new file mode 100644
index 000000000..3f13b2774
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java
@@ -0,0 +1,29 @@
+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;
+
+ /**
+ * 实际响应体
+ */
+ private Object response;
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/exception/SmsException.java b/ruoyi/src/main/java/com/ruoyi/sms/exception/SmsException.java
new file mode 100644
index 000000000..28632a375
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/sms/exception/SmsException.java
@@ -0,0 +1,16 @@
+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);
+ }
+
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/domain/SysOssConfig.java b/ruoyi/src/main/java/com/ruoyi/system/domain/SysOssConfig.java
index a8340df77..577f17fb8 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/domain/SysOssConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/domain/SysOssConfig.java
@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import lombok.experimental.Accessors;
/**
* 对象存储配置对象 sys_oss_config
@@ -53,6 +52,11 @@ public class SysOssConfig extends BaseEntity {
*/
private String endpoint;
+ /**
+ * 自定义域名
+ */
+ private String domain;
+
/**
* 是否https(0否 1是)
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java b/ruoyi/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java
index 9a66e384b..5ac4e96e4 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java
@@ -35,8 +35,8 @@ public class SysOssConfigBo extends BaseEntity {
/**
* 配置key
*/
- @ApiModelProperty(value = "configKey", required = true)
- @NotBlank(message = "configKey不能为空", groups = {AddGroup.class, EditGroup.class})
+ @ApiModelProperty(value = "配置key", required = true)
+ @NotBlank(message = "配置key不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "configKey长度必须介于2和20 之间")
private String configKey;
@@ -59,8 +59,8 @@ public class SysOssConfigBo extends BaseEntity {
/**
* 桶名称
*/
- @ApiModelProperty(value = "bucketName", required = true)
- @NotBlank(message = "bucketName不能为空", groups = {AddGroup.class, EditGroup.class})
+ @ApiModelProperty(value = "桶名称", required = true)
+ @NotBlank(message = "桶名称不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "bucketName长度必须介于2和100之间")
private String bucketName;
@@ -73,11 +73,17 @@ public class SysOssConfigBo extends BaseEntity {
/**
* 访问站点
*/
- @ApiModelProperty(value = "endpoint", required = true)
- @NotBlank(message = "endpoint不能为空", groups = {AddGroup.class, EditGroup.class})
+ @ApiModelProperty(value = "访问站点", required = true)
+ @NotBlank(message = "访问站点不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "endpoint长度必须介于2和100之间")
private String endpoint;
+ /**
+ * 自定义域名
+ */
+ @ApiModelProperty("自定义域名")
+ private String domain;
+
/**
* 是否https(Y=是,N=否)
*/
@@ -93,7 +99,7 @@ public class SysOssConfigBo extends BaseEntity {
/**
* 域
*/
- @ApiModelProperty(value = "region")
+ @ApiModelProperty(value = "域")
private String region;
/**
@@ -102,4 +108,10 @@ public class SysOssConfigBo extends BaseEntity {
@ApiModelProperty(value = "扩展字段")
private String ext1;
+ /**
+ * 备注
+ */
+ @ApiModelProperty(value = "备注")
+ private String remark;
+
}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java b/ruoyi/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java
index 0fb08dd00..20edacaa6 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java
@@ -62,6 +62,12 @@ public class SysOssConfigVo {
@ApiModelProperty("访问站点")
private String endpoint;
+ /**
+ * 自定义域名
+ */
+ @ApiModelProperty("自定义域名")
+ private String domain;
+
/**
* 是否https(Y=是,N=否)
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java b/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java
index b444e6d06..c55e5bc12 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java
@@ -8,6 +8,7 @@ import com.ruoyi.system.domain.vo.SysOssVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
+import java.util.List;
/**
* 文件上传 服务层
@@ -18,6 +19,8 @@ public interface ISysOssService {
TableDataInfo queryPageList(SysOssBo sysOss, PageQuery pageQuery);
+ List listByIds(Collection ossIds);
+
SysOss getById(Long ossId);
SysOss upload(MultipartFile file);
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java b/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java
index e0b53c1fb..1a672e59c 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java
@@ -27,8 +27,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
+import java.time.Duration;
import java.util.List;
-import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
@@ -79,7 +79,7 @@ public class SysLoginService {
SysUser user = loadUserByPhonenumber(phonenumber);
HttpServletRequest request = ServletUtils.getRequest();
- checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
+ checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode, request));
// 此处可根据登录用户的数据不同 自行创建 loginUser
LoginUser loginUser = buildLoginUser(user);
// 生成token
@@ -121,9 +121,13 @@ public class SysLoginService {
/**
* 校验短信验证码
*/
- private boolean validateSmsCode(String phonenumber, String smsCode) {
- // todo 此处使用手机号查询redis验证码与参数验证码是否一致 用户自行实现
- return true;
+ private boolean validateSmsCode(String phonenumber, String smsCode, HttpServletRequest request) {
+ String code = RedisUtils.getCacheObject(Constants.CAPTCHA_CODE_KEY + phonenumber);
+ if (StringUtils.isBlank(code)) {
+ asyncService.recordLogininfor(phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"), request);
+ throw new CaptchaExpireException();
+ }
+ return code.equals(smsCode);
}
/**
@@ -205,7 +209,7 @@ public class SysLoginService {
loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(user));
loginUser.setRolePermission(permissionService.getRolePermission(user));
- loginUser.setDeptName(user.getDept().getDeptName());
+ loginUser.setDeptName(ObjectUtil.isNull(user.getDept()) ? "" : user.getDept().getDeptName());
List roles = BeanUtil.copyToList(user.getRoles(), RoleDTO.class);
loginUser.setRoles(roles);
return loginUser;
@@ -248,7 +252,7 @@ public class SysLoginService {
errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
// 达到规定错误次数 则锁定登录
if (errorNumber.equals(setErrorNumber)) {
- RedisUtils.setCacheObject(errorKey, errorNumber, errorLimitTime, TimeUnit.MINUTES);
+ RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(errorLimitTime));
asyncService.recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), errorLimitTime), request);
throw new UserException(loginType.getRetryLimitExceed(), errorLimitTime);
} else {
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
index 2f9d0020a..6525d31e9 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
@@ -68,7 +68,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
.orderByAsc(SysMenu::getOrderNum));
} else {
QueryWrapper wrapper = Wrappers.query();
- wrapper.eq("ur.user_id", userId)
+ wrapper.eq("sur.user_id", userId)
.like(StringUtils.isNotBlank(menu.getMenuName()), "m.menu_name", menu.getMenuName())
.eq(StringUtils.isNotBlank(menu.getVisible()), "m.visible", menu.getVisible())
.eq(StringUtils.isNotBlank(menu.getStatus()), "m.status", menu.getStatus())
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java
index f78be1e83..015c156ab 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java
@@ -95,9 +95,10 @@ public class SysOssConfigServiceImpl implements ISysOssConfigService {
SysOssConfig config = BeanUtil.toBean(bo, SysOssConfig.class);
validEntityBeforeSave(config);
LambdaUpdateWrapper luw = new LambdaUpdateWrapper<>();
- luw.set(StringUtils.isBlank(config.getPrefix()), SysOssConfig::getPrefix, "");
- luw.set(StringUtils.isBlank(config.getRegion()), SysOssConfig::getRegion, "");
- luw.set(StringUtils.isBlank(config.getExt1()), SysOssConfig::getExt1, "");
+ luw.set(ObjectUtil.isNull(config.getPrefix()), SysOssConfig::getPrefix, "");
+ luw.set(ObjectUtil.isNull(config.getRegion()), SysOssConfig::getRegion, "");
+ luw.set(ObjectUtil.isNull(config.getExt1()), SysOssConfig::getExt1, "");
+ luw.set(ObjectUtil.isNull(config.getRemark()), SysOssConfig::getRemark, "");
luw.eq(SysOssConfig::getOssConfigId, config.getOssConfigId());
return setConfigCache(baseMapper.update(config, luw) > 0, config);
}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
index 19423f404..03a6cfca8 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.service.impl;
+import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -7,9 +8,11 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.redis.RedisUtils;
+import com.ruoyi.oss.constant.OssConstant;
+import com.ruoyi.oss.core.OssClient;
import com.ruoyi.oss.entity.UploadResult;
import com.ruoyi.oss.factory.OssFactory;
-import com.ruoyi.oss.service.IOssStrategy;
import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssVo;
@@ -20,6 +23,8 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -42,6 +47,21 @@ public class SysOssServiceImpl implements ISysOssService {
return TableDataInfo.build(result);
}
+ @Override
+ public List listByIds(Collection ossIds) {
+ List list = new ArrayList<>();
+ for (Long id : ossIds) {
+ String key = OssConstant.SYS_OSS_KEY + id;
+ SysOssVo vo = RedisUtils.getCacheObject(key);
+ if (ObjectUtil.isNull(vo)) {
+ vo = baseMapper.selectVoById(id);
+ RedisUtils.setCacheObject(key, vo, Duration.ofDays(30));
+ }
+ list.add(vo);
+ }
+ return list;
+ }
+
private LambdaQueryWrapper buildQueryWrapper(SysOssBo bo) {
Map params = bo.getParams();
LambdaQueryWrapper lqw = Wrappers.lambdaQuery();
@@ -65,7 +85,7 @@ public class SysOssServiceImpl implements ISysOssService {
public SysOss upload(MultipartFile file) {
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
- IOssStrategy storage = OssFactory.instance();
+ OssClient storage = OssFactory.instance();
UploadResult uploadResult;
try {
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
@@ -78,7 +98,7 @@ public class SysOssServiceImpl implements ISysOssService {
oss.setFileSuffix(suffix);
oss.setFileName(uploadResult.getFilename());
oss.setOriginalName(originalfileName);
- oss.setService(storage.getServiceType().getValue());
+ oss.setService(storage.getConfigKey());
baseMapper.insert(oss);
return oss;
}
@@ -90,7 +110,7 @@ public class SysOssServiceImpl implements ISysOssService {
}
List list = baseMapper.selectBatchIds(ids);
for (SysOss sysOss : list) {
- IOssStrategy storage = OssFactory.instance(sysOss.getService());
+ OssClient storage = OssFactory.instance(sysOss.getService());
storage.delete(sysOss.getUrl());
}
return baseMapper.deleteBatchIds(ids) > 0;
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysSensitiveServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysSensitiveServiceImpl.java
index 08d4cc4f9..fe142ca3c 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysSensitiveServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysSensitiveServiceImpl.java
@@ -20,7 +20,7 @@ public class SysSensitiveServiceImpl implements SensitiveService {
*/
@Override
public boolean isSensitive() {
- return LoginHelper.isAdmin();
+ return !LoginHelper.isAdmin();
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
index 2ef63881c..ed24ab4e9 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.service.impl;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -115,11 +116,11 @@ public class SysUserServiceImpl implements ISysUserService {
*/
@Override
public TableDataInfo selectUnallocatedList(SysUser user, PageQuery pageQuery) {
- List userId = userRoleMapper.selectUserIdsByRoleId(user.getRoleId());
+ List userIds = userRoleMapper.selectUserIdsByRoleId(user.getRoleId());
QueryWrapper wrapper = Wrappers.query();
wrapper.eq("u.del_flag", UserConstants.USER_NORMAL)
.and(w -> w.ne("r.role_id", user.getRoleId()).or().isNull("r.role_id"))
- .notIn("u.user_id", userId)
+ .notIn(CollUtil.isNotEmpty(userIds), "u.user_id", userIds)
.like(StringUtils.isNotBlank(user.getUserName()), "u.user_name", user.getUserName())
.like(StringUtils.isNotBlank(user.getPhonenumber()), "u.phonenumber", user.getPhonenumber());
Page page = baseMapper.selectUnallocatedList(pageQuery.build(), wrapper);
@@ -402,20 +403,7 @@ public class SysUserServiceImpl implements ISysUserService {
* @param user 用户对象
*/
public void insertUserRole(SysUser user) {
- Long[] roles = user.getRoleIds();
- if (ObjectUtil.isNotNull(roles)) {
- // 新增用户与角色管理
- List list = new ArrayList();
- for (Long roleId : roles) {
- SysUserRole ur = new SysUserRole();
- ur.setUserId(user.getUserId());
- ur.setRoleId(roleId);
- list.add(ur);
- }
- if (list.size() > 0) {
- userRoleMapper.insertBatch(list);
- }
- }
+ this.insertUserRole(user.getUserId(), user.getRoleIds());
}
/**
@@ -425,18 +413,16 @@ public class SysUserServiceImpl implements ISysUserService {
*/
public void insertUserPost(SysUser user) {
Long[] posts = user.getPostIds();
- if (ObjectUtil.isNotNull(posts)) {
+ if (ArrayUtil.isNotEmpty(posts)) {
// 新增用户与岗位管理
- List list = new ArrayList();
+ List list = new ArrayList<>(posts.length);
for (Long postId : posts) {
SysUserPost up = new SysUserPost();
up.setUserId(user.getUserId());
up.setPostId(postId);
list.add(up);
}
- if (list.size() > 0) {
- userPostMapper.insertBatch(list);
- }
+ userPostMapper.insertBatch(list);
}
}
@@ -447,18 +433,16 @@ public class SysUserServiceImpl implements ISysUserService {
* @param roleIds 角色组
*/
public void insertUserRole(Long userId, Long[] roleIds) {
- if (ObjectUtil.isNotNull(roleIds)) {
+ if (ArrayUtil.isNotEmpty(roleIds)) {
// 新增用户与角色管理
- List list = new ArrayList();
+ List list = new ArrayList<>(roleIds.length);
for (Long roleId : roleIds) {
SysUserRole ur = new SysUserRole();
ur.setUserId(userId);
ur.setRoleId(roleId);
list.add(ur);
}
- if (list.size() > 0) {
- userRoleMapper.insertBatch(list);
- }
+ userRoleMapper.insertBatch(list);
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
index a19be65ea..0889aabc7 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
@@ -4,6 +4,8 @@ import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.enums.CaptchaType;
@@ -12,30 +14,68 @@ import com.ruoyi.common.utils.redis.RedisUtils;
import com.ruoyi.common.utils.reflect.ReflectUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.CaptchaProperties;
+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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
+import javax.validation.constraints.NotBlank;
+import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
/**
* 验证码操作处理
*
* @author Lion Li
*/
+@Anonymous
+@Slf4j
+@Validated
@Api(value = "验证码操作处理", tags = {"验证码管理"})
@RequiredArgsConstructor
@RestController
public class CaptchaController {
private final CaptchaProperties captchaProperties;
+ private final SmsProperties smsProperties;
private final ISysConfigService configService;
+ /**
+ * 短信验证码
+ */
+ @ApiOperation("短信验证码")
+ @GetMapping("/captchaSms")
+ public R smsCaptcha(@ApiParam("用户手机号")
+ @NotBlank(message = "{user.phonenumber.not.blank}")
+ String phonenumber) {
+ if (smsProperties.getEnabled()) {
+ R.fail("当前系统没有开启短信功能!");
+ }
+ String key = Constants.CAPTCHA_CODE_KEY + phonenumber;
+ String code = RandomUtil.randomNumbers(4);
+ RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
+ // 验证码模板id 自行处理 (查数据库或写死均可)
+ String templateId = "";
+ Map map = new HashMap<>(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());
+ }
+ return R.ok();
+ }
+
/**
* 生成验证码
*/
@@ -60,7 +100,7 @@ public class CaptchaController {
captcha.setGenerator(codeGenerator);
captcha.createCode();
String code = isMath ? getCodeResult(captcha.getCode()) : captcha.getCode();
- RedisUtils.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+ RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
ajax.put("uuid", uuid);
ajax.put("img", captcha.getImageBase64());
return R.ok(ajax);
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
index facf6ce99..cdb1d6cd6 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java
@@ -2,6 +2,7 @@ package com.ruoyi.web.controller.system;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
+import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysMenu;
@@ -51,6 +52,7 @@ public class SysLoginController {
* @param loginBody 登录信息
* @return 结果
*/
+ @Anonymous
@ApiOperation("登录方法")
@PostMapping("/login")
public R> login(@Validated @RequestBody LoginBody loginBody) {
@@ -68,6 +70,7 @@ public class SysLoginController {
* @param smsLoginBody 登录信息
* @return 结果
*/
+ @Anonymous
@ApiOperation("短信登录(示例)")
@PostMapping("/smsLogin")
public R> smsLogin(@Validated @RequestBody SmsLoginBody smsLoginBody) {
@@ -84,6 +87,7 @@ public class SysLoginController {
* @param xcxCode 小程序code
* @return 结果
*/
+ @Anonymous
@ApiOperation("小程序登录(示例)")
@PostMapping("/xcxLogin")
public R> xcxLogin(@NotBlank(message = "{xcx.code.not.blank}") String xcxCode) {
@@ -94,12 +98,14 @@ public class SysLoginController {
return R.ok(ajax);
}
+ @Anonymous
@ApiOperation("登出方法")
@PostMapping("/logout")
public R logout() {
try {
+ String username = LoginHelper.getUsername();
StpUtil.logout();
- loginService.logout(LoginHelper.getUsername());
+ loginService.logout(username);
} catch (NotLoginException e) {
}
return R.ok("退出成功");
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysOssController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysOssController.java
index a128a15ff..d4e52acc6 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysOssController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysOssController.java
@@ -32,6 +32,7 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/**
@@ -58,6 +59,19 @@ public class SysOssController extends BaseController {
return iSysOssService.queryPageList(bo, pageQuery);
}
+ /**
+ * 查询OSS对象基于id串
+ */
+ @ApiOperation("查询OSS对象基于ID")
+ @SaCheckPermission("system:oss:list")
+ @GetMapping("/listByIds/{ossIds}")
+ public R> listByIds(@ApiParam("OSS对象ID串")
+ @NotEmpty(message = "主键不能为空")
+ @PathVariable Long[] ossIds) {
+ List list = iSysOssService.listByIds(Arrays.asList(ossIds));
+ return R.ok(list);
+ }
+
/**
* 上传OSS对象存储
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
index 84d4c0b3d..22b506f36 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java
@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.system;
import cn.dev33.satoken.secure.BCrypt;
+import cn.hutool.core.io.FileUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
@@ -9,6 +10,7 @@ import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.helper.LoginHelper;
import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.service.ISysOssService;
import com.ruoyi.system.service.ISysUserService;
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
@@ -117,6 +120,10 @@ public class SysProfileController extends BaseController {
public R> avatar(@RequestPart("avatarfile") MultipartFile file) {
Map ajax = new HashMap<>();
if (!file.isEmpty()) {
+ String extension = FileUtil.extName(file.getOriginalFilename());
+ if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
+ return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
+ }
SysOss oss = iSysOssService.upload(file);
String avatar = oss.getUrl();
if (userService.updateUserAvatar(getUsername(), avatar)) {
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
index 6595f06df..b8cedd67a 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
@@ -1,5 +1,6 @@
package com.ruoyi.web.controller.system;
+import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.model.RegisterBody;
@@ -27,6 +28,7 @@ public class SysRegisterController extends BaseController {
private final SysRegisterService registerService;
private final ISysConfigService configService;
+ @Anonymous
@ApiOperation("用户注册")
@PostMapping("/register")
public R register(@Validated @RequestBody RegisterBody user) {
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
index 5d9a20615..9b7cb268b 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
@@ -148,7 +148,7 @@ public class SysRoleController extends BaseController {
@SaCheckPermission("system:role:remove")
@Log(title = "角色管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{roleIds}")
- public R remove(@ApiParam("岗位ID串") @PathVariable Long[] roleIds) {
+ public R remove(@ApiParam("角色ID串") @PathVariable Long[] roleIds) {
return toAjax(roleService.deleteRoleByIds(roleIds));
}
diff --git a/ruoyi/src/main/resources/application-dev.yml b/ruoyi/src/main/resources/application-dev.yml
index 45eb5be97..f9327f779 100644
--- a/ruoyi/src/main/resources/application-dev.yml
+++ b/ruoyi/src/main/resources/application-dev.yml
@@ -130,8 +130,8 @@ spring:
port: 6379
# 数据库索引
database: 0
- # 密码
- password:
+ # 密码(如没有密码请注释掉)
+ # password:
# 连接超时时间
timeout: 10s
# 是否开启ssl
@@ -156,3 +156,37 @@ redisson:
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
+
+--- # mail 邮件发送
+mail:
+ enabled: false
+ host: smtp.163.com
+ port: 465
+ # 是否需要用户名密码验证
+ auth: true
+ # 发送方,遵循RFC-822标准
+ from: xxx@163.com
+ # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
+ user: xxx@163.com
+ # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
+ pass: xxxxxxxxxx
+ # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
+ starttlsEnable: true
+ # 使用SSL安全连接
+ sslEnable: true
+ # SMTP超时时长,单位毫秒,缺省值不超时
+ timeout: 0
+ # Socket连接超时值,单位毫秒,缺省值不超时
+ connectionTimeout: 0
+
+--- # sms 短信
+sms:
+ enabled: false
+ # 阿里云 dysmsapi.aliyuncs.com
+ # 腾讯云 sms.tencentcloudapi.com
+ endpoint: "dysmsapi.aliyuncs.com"
+ accessKeyId: xxxxxxx
+ accessKeySecret: xxxxxx
+ signName: 测试
+ # 腾讯专用
+ sdkAppId:
diff --git a/ruoyi/src/main/resources/application-prod.yml b/ruoyi/src/main/resources/application-prod.yml
index 6a28bccfb..def5f1666 100644
--- a/ruoyi/src/main/resources/application-prod.yml
+++ b/ruoyi/src/main/resources/application-prod.yml
@@ -133,8 +133,8 @@ spring:
port: 6379
# 数据库索引
database: 0
- # 密码
- password:
+ # 密码(如没有密码请注释掉)
+ # password:
# 连接超时时间
timeout: 10s
# 是否开启ssl
@@ -159,3 +159,37 @@ redisson:
timeout: 3000
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
+
+--- # mail 邮件发送
+mail:
+ enabled: false
+ host: smtp.163.com
+ port: 465
+ # 是否需要用户名密码验证
+ auth: true
+ # 发送方,遵循RFC-822标准
+ from: xxx@163.com
+ # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
+ user: xxx@163.com
+ # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
+ pass: xxxxxxxxxx
+ # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
+ starttlsEnable: true
+ # 使用SSL安全连接
+ sslEnable: true
+ # SMTP超时时长,单位毫秒,缺省值不超时
+ timeout: 0
+ # Socket连接超时值,单位毫秒,缺省值不超时
+ connectionTimeout: 0
+
+--- # sms 短信
+sms:
+ enabled: false
+ # 阿里云 dysmsapi.aliyuncs.com
+ # 腾讯云 sms.tencentcloudapi.com
+ endpoint: "dysmsapi.aliyuncs.com"
+ accessKeyId: xxxxxxx
+ accessKeySecret: xxxxxx
+ signName: 测试
+ # 腾讯专用
+ sdkAppId:
diff --git a/ruoyi/src/main/resources/application.yml b/ruoyi/src/main/resources/application.yml
index c692fc267..ceaaf9561 100644
--- a/ruoyi/src/main/resources/application.yml
+++ b/ruoyi/src/main/resources/application.yml
@@ -118,12 +118,6 @@ sa-token:
security:
# 排除路径
excludes:
- - /login
- - /smsLogin
- - /xcxLogin
- - /logout
- - /register
- - /captchaImage
# 静态资源
- /*.html
- /**/*.html
diff --git a/ruoyi/src/main/resources/excel/单列表.xlsx b/ruoyi/src/main/resources/excel/单列表.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..0f7347d652c06ae29eca9fa6cd1055ec6ae6c6e3
GIT binary patch
literal 10787
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
NOlDzVs8R*l4gkIgmH7Yw
literal 0
HcmV?d00001
diff --git a/ruoyi/src/main/resources/excel/多列表.xlsx b/ruoyi/src/main/resources/excel/多列表.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..c7d11dcce51c7ebc9237827cd6caac0d4b474012
GIT binary patch
literal 10761
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