options) {
+ this.dropDownOptions = options;
+ this.currentOptionsColumnIndex = 0;
+ this.currentLinkedOptionsSheetIndex = 0;
+ this.dictService = SpringUtils.getBean(DictService.class);
+ }
+
+ /**
+ * 开始创建下拉数据
+ * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
+ * 如果有且设置了value值,则将其直接置为下拉可选项
+ *
+ * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
+ *
+ * 3.二者并存,注意调用方式
+ */
+ @Override
+ public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+ Sheet sheet = writeSheetHolder.getSheet();
+ // 开始设置下拉框 HSSFWorkbook
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
+ Workbook workbook = writeWorkbookHolder.getWorkbook();
+ int length = fields.length;
+ for (int i = 0; i < length; i++) {
+ // 循环实体中的每个属性
+ // 可选的下拉值
+ List options = new ArrayList<>();
+ if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
+ // 如果指定了@ExcelDictFormat,则使用字典的逻辑
+ ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
+ String dictType = format.dictType();
+ String converterExp = format.readConverterExp();
+ if (StrUtil.isNotBlank(dictType)) {
+ // 如果传递了字典名,则依据字典建立下拉
+ Collection values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+ .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
+ .values();
+ options = new ArrayList<>(values);
+ } else if (StrUtil.isNotBlank(converterExp)) {
+ // 如果指定了确切的值,则直接解析确切的值
+ options = StrUtil.split(converterExp, format.separator(), true, true);
+ }
+ } else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
+ // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
+ ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
+ List values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
+ options = StreamUtils.toList(values, String::valueOf);
+ }
+ if (ObjectUtil.isNotEmpty(options)) {
+ // 仅当下拉可选项不为空时执行
+ // 获取列下标,默认为当前循环次数
+ int index = i;
+ if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
+ // 如果指定了列下标,以指定的为主
+ index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
+ }
+ if (options.size() > 20) {
+ // 这里限制如果可选项大于20,则使用额外表形式
+ dropDownWithSheet(helper, workbook, sheet, index, options);
+ } else {
+ // 否则使用固定值形式
+ dropDownWithSimple(helper, sheet, index, options);
+ }
+ }
+ }
+ dropDownOptions.forEach(everyOptions -> {
+ // 如果传递了下拉框选择器参数
+ if (!everyOptions.getNextOptions().isEmpty()) {
+ // 当二级选项不为空时,使用额外关联表的形式
+ dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
+ } else if (everyOptions.getOptions().size() > 10) {
+ // 当一级选项参数个数大于10,使用额外表的形式
+ dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ } else if (everyOptions.getOptions().size() != 0) {
+ // 当一级选项个数不为空,使用默认形式
+ dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ }
+ });
+ }
+
+ /**
+ * 简单下拉框
+ * 直接将可选项拼接为指定列的数据校验值
+ *
+ * @param celIndex 列index
+ * @param value 下拉选可选值
+ */
+ private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List value) {
+ if (ObjectUtil.isEmpty(value)) {
+ return;
+ }
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+ }
+
+ /**
+ * 额外表格形式的级联下拉框
+ *
+ * @param options 额外表格形式存储的下拉可选项
+ */
+ private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
+ String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
+ // 创建联动下拉数据表
+ Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
+ // 将下拉表隐藏
+ workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
+ // 完善横向的一级选项数据表
+ List firstOptions = options.getOptions();
+ Map> secoundOptionsMap = options.getNextOptions();
+
+ // 创建名称管理器
+ Name name = workbook.createName();
+ // 设置名称管理器的别名
+ name.setNameName(linkedOptionsSheetName);
+ // 以横向第一行创建一级下拉拼接引用位置
+ String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
+ linkedOptionsSheetName,
+ getExcelColumnName(0),
+ getExcelColumnName(firstOptions.size())
+ );
+ // 设置名称管理器的引用位置
+ name.setRefersToFormula(firstOptionsFunction);
+ // 设置数据校验为序列模式,引用的是名称管理器中的别名
+ this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
+
+ for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
+ // 先提取主表中一级下拉的列名
+ String firstOptionsColumnName = getExcelColumnName(columIndex);
+ // 一次循环是每一个一级选项
+ int finalI = columIndex;
+ // 本次循环的一级选项值
+ String thisFirstOptionsValue = firstOptions.get(columIndex);
+ // 创建第一行的数据
+ Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
+ // 如果不存在则创建第一行
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
+ // 第一行当前列
+ .createCell(columIndex)
+ // 设置值为当前一级选项值
+ .setCellValue(thisFirstOptionsValue);
+
+ // 第二行开始,设置第二级别选项参数
+ List secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+ if (CollUtil.isEmpty(secondOptions)) {
+ // 必须保证至少有一个关联选项,否则将导致Excel解析错误
+ secondOptions = Collections.singletonList("暂无_0");
+ }
+
+ // 以该一级选项值创建子名称管理器
+ Name sonName = workbook.createName();
+ // 设置名称管理器的别名
+ sonName.setNameName(thisFirstOptionsValue);
+ // 以第二行该列数据拼接引用位置
+ String sonFunction = String.format("%s!$%s$2:$%s$%d",
+ linkedOptionsSheetName,
+ firstOptionsColumnName,
+ firstOptionsColumnName,
+ secondOptions.size() + 1
+ );
+ // 设置名称管理器的引用位置
+ sonName.setRefersToFormula(sonFunction);
+ // 数据验证为序列模式,引用到每一个主表中的二级选项位置
+ // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
+ String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
+ for (int i = 0; i < 100; i++) {
+ // 以一级选项对应的主体所在位置创建二级下拉
+ String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
+ // 二级只能主表每一行的每一列添加二级校验
+ markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
+ }
+
+ for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
+ // 从第二行开始填充二级选项
+ int finalRowIndex = rowIndex + 1;
+ int finalColumIndex = columIndex;
+
+ Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
+ // 没有则创建
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
+ Optional
+ // 在本级一级选项所在的列
+ .ofNullable(row.getCell(finalColumIndex))
+ // 不存在则创建
+ .orElseGet(() -> row.createCell(finalColumIndex))
+ // 设置二级选项值
+ .setCellValue(secondOptions.get(rowIndex));
+ }
+ }
+
+ currentLinkedOptionsSheetIndex++;
+ }
+
+ /**
+ * 额外表格形式的普通下拉框
+ * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
+ *
+ * @param celIndex 下拉选
+ * @param value 下拉选可选值
+ */
+ private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List value) {
+ // 创建下拉数据表
+ Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
+ .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
+ // 将下拉表隐藏
+ workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
+ // 完善纵向的一级选项数据表
+ for (int i = 0; i < value.size(); i++) {
+ int finalI = i;
+ // 获取每一选项行,如果没有则创建
+ Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
+ .orElseGet(() -> simpleDataSheet.createRow(finalI));
+ // 获取本级选项对应的选项列,如果没有则创建
+ Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
+ .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
+ // 设置值
+ cell.setCellValue(value.get(i));
+ }
+
+ // 创建名称管理器
+ Name name = workbook.createName();
+ // 设置名称管理器的别名
+ String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
+ name.setNameName(nameName);
+ // 以纵向第一列创建一级下拉拼接引用位置
+ String function = String.format("%s!$%s$1:$%s$%d",
+ OPTIONS_SHEET_NAME,
+ getExcelColumnName(currentOptionsColumnIndex),
+ getExcelColumnName(currentOptionsColumnIndex),
+ value.size());
+ // 设置名称管理器的引用位置
+ name.setRefersToFormula(function);
+ // 设置数据校验为序列模式,引用的是名称管理器中的别名
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
+ currentOptionsColumnIndex++;
+ }
+
+ /**
+ * 挂载下拉的列,仅限一级选项
+ */
+ private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
+ DataValidationConstraint constraint) {
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 挂载下拉的列,仅限二级选项
+ */
+ private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
+ Integer celIndex, DataValidationConstraint constraint) {
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 应用数据校验
+ */
+ private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
+ DataValidationConstraint constraint, CellRangeAddressList addressList) {
+ // 数据有效性对象
+ DataValidation dataValidation = helper.createValidation(constraint, addressList);
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation) {
+ //数据校验
+ dataValidation.setSuppressDropDownArrow(true);
+ //错误提示
+ dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+ dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
+ dataValidation.setShowErrorBox(true);
+ //选定提示
+ dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
+ dataValidation.setShowPromptBox(true);
+ sheet.addValidationData(dataValidation);
+ } else {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+ sheet.addValidationData(dataValidation);
+ }
+
+ /**
+ * 依据列index获取列名英文
+ * 依据列index转换为Excel中的列名英文
+ * 例如第1列,index为0,解析出来为A列
+ * 第27列,index为26,解析为AA列
+ * 第28列,index为27,解析为AB列
+ *
+ * @param columnIndex 列index
+ * @return 列index所在得英文名
+ */
+ private String getExcelColumnName(int columnIndex) {
+ // 26一循环的次数
+ int columnCircleCount = columnIndex / 26;
+ // 26一循环内的位置
+ int thisCircleColumnIndex = columnIndex % 26;
+ // 26一循环的次数大于0,则视为栏名至少两位
+ String columnPrefix = columnCircleCount == 0
+ ? StrUtil.EMPTY
+ : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+ // 从26一循环内取对应的栏位名
+ String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+ // 将二者拼接即为最终的栏位名
+ return columnPrefix + columnNext;
+ }
+}
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 3cbddb137..b2a863eb9 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/helper/LoginHelper.java
@@ -2,6 +2,7 @@ package com.ruoyi.common.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
@@ -54,6 +55,14 @@ public class LoginHelper {
if (ObjectUtil.isNotNull(deviceType)) {
model.setDevice(deviceType.getDevice());
}
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+// UserType userType = UserType.getUserType(loginUser.getUserType());
+// if (userType == UserType.SYS_USER) {
+// model.setTimeout(86400).setActiveTimeout(1800);
+// } else if (userType == UserType.APP_USER) {
+// model.setTimeout(86400).setActiveTimeout(1800);
+// }
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
@@ -66,7 +75,11 @@ public class LoginHelper {
if (loginUser != null) {
return loginUser;
}
- loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
+ SaSession session = StpUtil.getTokenSession();
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
return loginUser;
}
@@ -75,7 +88,11 @@ public class LoginHelper {
* 获取用户基于token
*/
public static LoginUser getLoginUser(String token) {
- return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
+ SaSession session = StpUtil.getTokenSessionByToken(token);
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ return (LoginUser) session.get(LOGIN_USER_KEY);
}
/**
@@ -113,8 +130,8 @@ public class LoginHelper {
* 获取用户类型
*/
public static UserType getUserType() {
- String loginId = StpUtil.getLoginIdAsString();
- return UserType.getUserType(loginId);
+ String loginType = StpUtil.getLoginIdAsString();
+ return UserType.getUserType(loginType);
}
/**
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java b/ruoyi/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
index e1bafee22..d5b341419 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
@@ -17,11 +17,10 @@ import java.util.List;
import java.util.Map;
/**
- * bean深拷贝工具(基于 cglib 性能优异)
+ * bean拷贝工具(基于 cglib 性能优异)
*
* 重点 cglib 不支持 拷贝到链式对象
* 例如: 源对象 拷贝到 目标(链式对象)
- * 请区分好`浅拷贝`和`深拷贝`再做使用
*
* @author Lion Li
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/common/utils/StreamUtils.java b/ruoyi/src/main/java/com/ruoyi/common/utils/StreamUtils.java
index a03e5bee4..0f57122eb 100644
--- a/ruoyi/src/main/java/com/ruoyi/common/utils/StreamUtils.java
+++ b/ruoyi/src/main/java/com/ruoyi/common/utils/StreamUtils.java
@@ -30,6 +30,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
return collection.stream().filter(function).collect(Collectors.toList());
}
@@ -70,7 +71,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
- return collection.stream().sorted(comparing).collect(Collectors.toList());
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
}
/**
@@ -87,7 +89,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
- return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
}
/**
@@ -106,7 +108,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
- return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
}
/**
@@ -124,7 +126,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
}
@@ -145,7 +147,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
}
@@ -166,7 +168,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
}
@@ -188,6 +190,7 @@ public class StreamUtils {
.stream()
.map(function)
.filter(Objects::nonNull)
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
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 cf2ba885c..8459e1478 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
@@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.common.convert.ExcelBigNumberConvert;
-import com.ruoyi.common.excel.CellMergeStrategy;
-import com.ruoyi.common.excel.DefaultExcelListener;
-import com.ruoyi.common.excel.ExcelListener;
-import com.ruoyi.common.excel.ExcelResult;
+import com.ruoyi.common.excel.*;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import lombok.AccessLevel;
@@ -88,7 +85,26 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
- exportExcel(list, sheetName, clazz, false, os);
+ exportExcel(list, sheetName, clazz, false, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param response 响应体
+ * @param options 级联下拉选
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, HttpServletResponse response, List options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, false, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -107,7 +123,27 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
- exportExcel(list, sheetName, clazz, merge, os);
+ exportExcel(list, sheetName, clazz, merge, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param merge 是否合并单元格
+ * @param response 响应体
+ * @param options 级联下拉选
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, boolean merge, HttpServletResponse response, List options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, merge, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -122,7 +158,20 @@ public class ExcelUtil {
* @param os 输出流
*/
public static void exportExcel(List list, String sheetName, Class clazz, OutputStream os) {
- exportExcel(list, sheetName, clazz, false, os);
+ exportExcel(list, sheetName, clazz, false, os, null);
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param os 输出流
+ * @param options 级联下拉选内容
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, OutputStream os, List options) {
+ exportExcel(list, sheetName, clazz, false, os, options);
}
/**
@@ -134,7 +183,8 @@ public class ExcelUtil {
* @param merge 是否合并单元格
* @param os 输出流
*/
- public static void exportExcel(List list, String sheetName, Class clazz, boolean merge, OutputStream os) {
+ public static void exportExcel(List list, String sheetName, Class clazz, boolean merge,
+ OutputStream os, List options) {
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
.autoCloseStream(false)
// 自动适配
@@ -146,6 +196,10 @@ public class ExcelUtil {
// 合并处理器
builder.registerWriteHandler(new CellMergeStrategy(list, true));
}
+ if (CollUtil.isNotEmpty(options)) {
+ // 添加下拉框操作
+ builder.registerWriteHandler(new ExcelDownHandler(options));
+ }
builder.doWrite(list);
}
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 162380890..7a852bf90 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
@@ -129,6 +129,18 @@ public class RedisUtils {
batch.execute();
}
+ /**
+ * 如果不存在则设置 并返回 true 如果存在则返回 false
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ * @return set成功或失败
+ */
+ public static boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
+ RBucket bucket = CLIENT.getBucket(key);
+ return bucket.setIfAbsent(value, duration);
+ }
+
/**
* 注册对象监听器
*
@@ -374,6 +386,21 @@ public class RedisUtils {
return rMap.remove(hKey);
}
+ /**
+ * 删除Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键
+ */
+ public static void delMultiCacheMapValue(final String key, final Set hKeys) {
+ RBatch batch = CLIENT.createBatch();
+ RMapAsync rMap = batch.getMap(key);
+ for (String hKey : hKeys) {
+ rMap.removeAsync(hKey);
+ }
+ batch.execute();
+ }
+
/**
* 获取多个Hash中的数据
*
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java
index cc6012f44..8965a2d88 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/SmsController.java
@@ -1,17 +1,17 @@
package com.ruoyi.demo.controller;
import com.ruoyi.common.core.domain.R;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
import lombok.RequiredArgsConstructor;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.LinkedHashMap;
/**
* 短信演示案例
@@ -26,10 +26,6 @@ import java.util.Map;
@RequestMapping("/demo/sms")
public class SmsController {
- private final SmsProperties smsProperties;
-// private final SmsTemplate smsTemplate; // 可以使用spring注入
-// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
-
/**
* 发送短信Aliyun
*
@@ -38,17 +34,11 @@ public class SmsController {
*/
@GetMapping("/sendAliyun")
public R sendAliyun(String phones, String templateId) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
- if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
- return R.fail("阿里云依赖未引入!");
- }
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
map.put("code", "1234");
- Object send = smsTemplate.send(phones, templateId, map);
- return R.ok(send);
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+ SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+ return R.ok(smsResponse);
}
/**
@@ -59,18 +49,12 @@ public class SmsController {
*/
@GetMapping("/sendTencent")
public R sendTencent(String phones, String templateId) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
- if (!SpringUtils.containsBean("tencentSmsTemplate")) {
- return R.fail("腾讯云依赖未引入!");
- }
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
// map.put("2", "测试测试");
map.put("1", "1234");
- Object send = smsTemplate.send(phones, templateId, map);
- return R.ok(send);
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT);
+ SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+ return R.ok(smsResponse);
}
}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
index 51b81c582..e1ecb2074 100644
--- a/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
+++ b/ruoyi/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
@@ -1,12 +1,17 @@
package com.ruoyi.demo.controller;
import cn.hutool.core.collection.CollUtil;
+import com.ruoyi.common.excel.ExcelResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.listener.ExportDemoListener;
+import com.ruoyi.demo.service.IExportExcelService;
import lombok.AllArgsConstructor;
import lombok.Data;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
@@ -20,9 +25,12 @@ import java.util.Map;
* @author Lion Li
*/
@RestController
+@RequiredArgsConstructor
@RequestMapping("/demo/excel")
public class TestExcelController {
+ private final IExportExcelService exportExcelService;
+
/**
* 单列表多数据
*/
@@ -76,6 +84,26 @@ public class TestExcelController {
ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
}
+ /**
+ * 导出下拉框
+ *
+ * @param response /
+ */
+ @GetMapping("/exportWithOptions")
+ public void exportWithOptions(HttpServletResponse response) {
+ exportExcelService.exportWithOptions(response);
+ }
+
+ /**
+ * 导入表格
+ */
+ @PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ public List importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
+ // 处理解析结果
+ ExcelResult excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
+ return excelResult.getList();
+ }
+
@Data
@AllArgsConstructor
static class TestObj1 {
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java b/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
new file mode 100644
index 000000000..e417dbc4c
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
@@ -0,0 +1,119 @@
+package com.ruoyi.demo.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.convert.ExcelEnumConvert;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.enums.UserStatus;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 带有下拉选的Excel导出
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExportDemoVo {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户昵称
+ */
+ @ExcelProperty(value = "用户名", index = 0)
+ @NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
+ private String nickName;
+
+ /**
+ * 用户类型
+ *
+ * 使用ExcelEnumFormat注解需要进行下拉选的部分
+ */
+ @ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
+ @ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
+ @NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
+ private String userStatus;
+
+ /**
+ * 性别
+ *
+ * 使用ExcelDictFormat注解需要进行下拉选的部分
+ */
+ @ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_user_sex")
+ @NotEmpty(message = "性别不能为空", groups = AddGroup.class)
+ private String gender;
+
+ /**
+ * 手机号
+ */
+ @ExcelProperty(value = "手机号", index = 3)
+ @NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
+ private String phoneNumber;
+
+ /**
+ * Email
+ */
+ @ExcelProperty(value = "Email", index = 4)
+ @NotEmpty(message = "Email不能为空", groups = AddGroup.class)
+ private String email;
+
+ /**
+ * 省
+ *
+ * 级联下拉,仅判断是否选了
+ */
+ @ExcelProperty(value = "省", index = 5)
+ @NotNull(message = "省不能为空", groups = AddGroup.class)
+ private String province;
+
+ /**
+ * 数据库中的省ID
+ *
+ * 处理完毕后再判断是否市正确的值
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer provinceId;
+
+ /**
+ * 市
+ *
+ * 级联下拉
+ */
+ @ExcelProperty(value = "市", index = 6)
+ @NotNull(message = "市不能为空", groups = AddGroup.class)
+ private String city;
+
+ /**
+ * 数据库中的市ID
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer cityId;
+
+ /**
+ * 县
+ *
+ * 级联下拉
+ */
+ @ExcelProperty(value = "县", index = 7)
+ @NotNull(message = "县不能为空", groups = AddGroup.class)
+ private String area;
+
+ /**
+ * 数据库中的县ID
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer areaId;
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java b/ruoyi/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java
new file mode 100644
index 000000000..95d7093f2
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java
@@ -0,0 +1,68 @@
+package com.ruoyi.demo.listener;
+
+import cn.hutool.core.util.NumberUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.excel.DefaultExcelListener;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.ValidatorUtils;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+
+import java.util.List;
+
+/**
+ * Excel带下拉框的解析处理器
+ *
+ * @author Emil.Zhang
+ */
+public class ExportDemoListener extends DefaultExcelListener {
+
+ public ExportDemoListener() {
+ // 显示使用构造函数,否则将导致空指针
+ super(true);
+ }
+
+ @Override
+ public void invoke(ExportDemoVo data, AnalysisContext context) {
+ // 先校验必填
+ ValidatorUtils.validate(data, AddGroup.class);
+
+ // 处理级联下拉的部分
+ String province = data.getProvince();
+ String city = data.getCity();
+ String area = data.getArea();
+ // 本行用户选择的省
+ List thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
+ if (thisRowSelectedProvinceOption.size() == 2) {
+ String provinceIdStr = thisRowSelectedProvinceOption.get(1);
+ if (NumberUtil.isNumber(provinceIdStr)) {
+ // 严格要求数据的话可以在这里做与数据库相关的判断
+ // 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
+ data.setProvinceId(Integer.parseInt(provinceIdStr));
+ }
+ }
+ // 本行用户选择的市
+ List thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
+ if (thisRowSelectedCityOption.size() == 2) {
+ String cityIdStr = thisRowSelectedCityOption.get(1);
+ if (NumberUtil.isNumber(cityIdStr)) {
+ data.setCityId(Integer.parseInt(cityIdStr));
+ }
+ }
+ // 本行用户选择的县
+ List thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
+ if (thisRowSelectedAreaOption.size() == 2) {
+ String areaIdStr = thisRowSelectedAreaOption.get(1);
+ if (NumberUtil.isNumber(areaIdStr)) {
+ data.setAreaId(Integer.parseInt(areaIdStr));
+ }
+ }
+
+ // 处理完毕以后判断是否符合规则
+ ValidatorUtils.validate(data, EditGroup.class);
+
+ // 添加到处理结果中
+ getExcelResult().getList().add(data);
+ }
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/service/IExportExcelService.java b/ruoyi/src/main/java/com/ruoyi/demo/service/IExportExcelService.java
new file mode 100644
index 000000000..d8f024784
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/service/IExportExcelService.java
@@ -0,0 +1,18 @@
+package com.ruoyi.demo.service;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+public interface IExportExcelService {
+
+ /**
+ * 导出下拉框
+ *
+ * @param response /
+ */
+ void exportWithOptions(HttpServletResponse response);
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java
new file mode 100644
index 000000000..37221c18d
--- /dev/null
+++ b/ruoyi/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java
@@ -0,0 +1,223 @@
+package com.ruoyi.demo.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.StreamUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.service.IExportExcelService;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+@Service
+@RequiredArgsConstructor
+public class ExportExcelServiceImpl implements IExportExcelService {
+
+ @Override
+ public void exportWithOptions(HttpServletResponse response) {
+ // 创建表格数据,业务中一般通过数据库查询
+ List excelDataList = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ // 模拟数据库中的一条数据
+ ExportDemoVo everyRowData = new ExportDemoVo();
+ everyRowData.setNickName("用户-" + i);
+ everyRowData.setUserStatus(UserStatus.OK.getCode());
+ everyRowData.setGender("1");
+ everyRowData.setPhoneNumber(String.format("175%08d", i));
+ everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
+ everyRowData.setProvinceId(i);
+ everyRowData.setCityId(i);
+ everyRowData.setAreaId(i);
+ excelDataList.add(everyRowData);
+ }
+
+ // 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
+ // 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
+ // 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
+
+ // 首先从数据库中查询下拉框内的可选项
+ // 这里模拟查询结果
+ List provinceList = getProvinceList(),
+ cityList = getCityList(provinceList),
+ areaList = getAreaList(cityList);
+ int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
+
+ DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
+ provinceList,
+ provinceIndex,
+ cityList,
+ cityIndex,
+ DemoCityData::getId,
+ DemoCityData::getPid,
+ everyOptions -> DropDownOptions.createOptionValue(
+ everyOptions.getName(),
+ everyOptions.getId()
+ )
+ );
+
+ DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
+ cityList,
+ cityIndex,
+ areaList,
+ areaIndex,
+ DemoCityData::getId,
+ DemoCityData::getPid,
+ everyOptions -> DropDownOptions.createOptionValue(
+ everyOptions.getName(),
+ everyOptions.getId()
+ )
+ );
+
+ // 把所有的下拉框存储
+ List options = new ArrayList<>();
+ options.add(provinceToCity);
+ options.add(cityToArea);
+
+ // 到此为止所有的下拉框可选项已全部配置完毕
+
+ // 接下来需要将Excel中的展示数据转换为对应的下拉选
+ List outList = StreamUtils.toList(excelDataList, everyRowData -> {
+ // 只需要处理没有使用@ExcelDictFormat注解的下拉框
+ // 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
+ everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
+ everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
+ everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
+ return everyRowData;
+ });
+
+ ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
+ }
+
+ private String buildOptions(List cityDataList, Integer id) {
+ Map> groupByIdMap =
+ cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+ if (groupByIdMap.containsKey(id)) {
+ DemoCityData demoCityData = groupByIdMap.get(id).get(0);
+ return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
+ } else {
+ return StrUtil.EMPTY;
+ }
+ }
+
+ /**
+ * 模拟查询数据库操作
+ *
+ * @return /
+ */
+ private List getProvinceList() {
+ List provinceList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ provinceList.add(new DemoCityData(0, null, "安徽省"));
+ provinceList.add(new DemoCityData(1, null, "江苏省"));
+
+ return provinceList;
+ }
+
+ /**
+ * 模拟查找数据库操作,需要连带查询出省的数据
+ *
+ * @param provinceList 模拟的父省数据
+ * @return /
+ */
+ private List getCityList(List provinceList) {
+ List cityList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ cityList.add(new DemoCityData(0, 0, "合肥市"));
+ cityList.add(new DemoCityData(1, 0, "芜湖市"));
+ cityList.add(new DemoCityData(2, 1, "南京市"));
+ cityList.add(new DemoCityData(3, 1, "无锡市"));
+ cityList.add(new DemoCityData(4, 1, "徐州市"));
+
+ selectParentData(provinceList, cityList);
+
+ return cityList;
+ }
+
+ /**
+ * 模拟查找数据库操作,需要连带查询出市的数据
+ *
+ * @param cityList 模拟的父市数据
+ * @return /
+ */
+ private List getAreaList(List cityList) {
+ List areaList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ areaList.add(new DemoCityData(0, 0, "瑶海区"));
+ areaList.add(new DemoCityData(1, 0, "庐江区"));
+ areaList.add(new DemoCityData(2, 1, "南宁县"));
+ areaList.add(new DemoCityData(3, 1, "镜湖区"));
+ areaList.add(new DemoCityData(4, 2, "玄武区"));
+ areaList.add(new DemoCityData(5, 2, "秦淮区"));
+ areaList.add(new DemoCityData(6, 3, "宜兴市"));
+ areaList.add(new DemoCityData(7, 3, "新吴区"));
+ areaList.add(new DemoCityData(8, 4, "鼓楼区"));
+ areaList.add(new DemoCityData(9, 4, "丰县"));
+
+ selectParentData(cityList, areaList);
+
+ return areaList;
+ }
+
+ /**
+ * 模拟数据库的查询父数据操作
+ *
+ * @param parentList /
+ * @param sonList /
+ */
+ private void selectParentData(List parentList, List sonList) {
+ Map> parentGroupByIdMap =
+ parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+
+ sonList.forEach(everySon -> {
+ if (parentGroupByIdMap.containsKey(everySon.getPid())) {
+ everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
+ }
+ });
+ }
+
+ /**
+ * 模拟的数据库省市县
+ */
+ @Data
+ private static class DemoCityData {
+ /**
+ * 数据库id字段
+ */
+ private Integer id;
+ /**
+ * 数据库pid字段
+ */
+ private Integer pid;
+ /**
+ * 数据库name字段
+ */
+ private String name;
+ /**
+ * MyBatisPlus连带查询父数据
+ */
+ private DemoCityData pData;
+
+ public DemoCityData(Integer id, Integer pid, String name) {
+ this.id = id;
+ this.pid = pid;
+ this.name = name;
+ }
+ }
+}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
index 00918327b..1e0c05285 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
@@ -2,6 +2,7 @@ package com.ruoyi.framework.aspectj;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.event.OperLogEvent;
@@ -25,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;
+import java.util.StringJoiner;
/**
* 操作日志记录处理
@@ -144,26 +146,23 @@ public class LogAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
- StringBuilder params = new StringBuilder();
- if (paramsArray != null && paramsArray.length > 0) {
- for (Object o : paramsArray) {
- if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
- try {
- String str = JsonUtils.toJsonString(o);
- Dict dict = JsonUtils.parseMap(str);
- if (MapUtil.isNotEmpty(dict)) {
- MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
- MapUtil.removeAny(dict, excludeParamNames);
- str = JsonUtils.toJsonString(dict);
- }
- params.append(str).append(" ");
- } catch (Exception e) {
- e.printStackTrace();
- }
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ for (Object o : paramsArray) {
+ if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+ String str = JsonUtils.toJsonString(o);
+ Dict dict = JsonUtils.parseMap(str);
+ if (MapUtil.isNotEmpty(dict)) {
+ MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
+ MapUtil.removeAny(dict, excludeParamNames);
+ str = JsonUtils.toJsonString(dict);
}
+ params.add(str);
}
}
- return params.toString().trim();
+ return params.toString();
}
/**
@@ -184,9 +183,8 @@ public class LogAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
- for (Object value : map.entrySet()) {
- Map.Entry entry = (Map.Entry) value;
- return entry.getValue() instanceof MultipartFile;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
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 7c2fa2a41..4c4249c30 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
@@ -1,6 +1,7 @@
package com.ruoyi.framework.aspectj;
import cn.dev33.satoken.SaManager;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.ruoyi.common.annotation.RepeatSubmit;
@@ -12,8 +13,6 @@ import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.redis.RedisUtils;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
@@ -28,14 +27,13 @@ import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
+import java.util.StringJoiner;
/**
* 防止重复提交(参考美团GTIS防重系统)
*
* @author Lion Li
*/
-@Slf4j
-@RequiredArgsConstructor
@Aspect
@Component
public class RepeatSubmitAspect {
@@ -45,10 +43,8 @@ public class RepeatSubmitAspect {
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
- long interval = 0;
- if (repeatSubmit.interval() > 0) {
- interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
- }
+ long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
+
if (interval < 1000) {
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
}
@@ -64,9 +60,7 @@ public class RepeatSubmitAspect {
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
- String key = RedisUtils.getCacheObject(cacheRepeatKey);
- if (key == null) {
- RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
+ if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
@@ -114,19 +108,16 @@ public class RepeatSubmitAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
- StringBuilder params = new StringBuilder();
- if (paramsArray != null && paramsArray.length > 0) {
- for (Object o : paramsArray) {
- if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
- try {
- params.append(JsonUtils.toJsonString(o)).append(" ");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ for (Object o : paramsArray) {
+ if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+ params.add(JsonUtils.toJsonString(o));
}
}
- return params.toString().trim();
+ return params.toString();
}
/**
@@ -147,9 +138,8 @@ public class RepeatSubmitAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
- for (Object value : map.entrySet()) {
- Map.Entry entry = (Map.Entry) value;
- return entry.getValue() instanceof MultipartFile;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java b/ruoyi/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
index d8bfce7c5..ddd0f33bb 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
@@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
@Override
public Cache getCache(String name) {
+ // 重写 cacheName 支持多参数
+ String[] array = StringUtils.delimitedListToStringArray(name, "#");
+ name = array[0];
+
Cache cache = instanceMap.get(name);
if (cache != null) {
return cache;
@@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
configMap.put(name, config);
}
- // 重写 cacheName 支持多参数
- String[] array = StringUtils.delimitedListToStringArray(name, "#");
- name = array[0];
if (array.length > 1) {
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
}
diff --git a/ruoyi/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
index 40f2d27c3..f1cbff22a 100644
--- a/ruoyi/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
+++ b/ruoyi/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
@@ -16,8 +16,10 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
@@ -108,6 +110,26 @@ public class GlobalExceptionHandler {
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
}
+ /**
+ * 请求路径中缺少必需的路径变量
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public R handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+ return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+ }
+
+ /**
+ * 请求参数类型不匹配
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+ return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
+ }
+
/**
* 拦截未知的运行时异常
*/
diff --git a/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java b/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java
index a82853e9c..bf2bb6c71 100644
--- a/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java
+++ b/ruoyi/src/main/java/com/ruoyi/oss/core/OssClient.java
@@ -24,6 +24,7 @@ import com.ruoyi.oss.exception.OssException;
import com.ruoyi.oss.properties.OssProperties;
import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
@@ -115,6 +116,18 @@ public class OssClient {
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
+ public UploadResult upload(File file, String path) {
+ try {
+ PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
+ // 设置上传对象的 Acl 为公共读
+ putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
+ client.putObject(putObjectRequest);
+ } catch (Exception e) {
+ throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+ }
+ return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
+ }
+
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
@@ -132,6 +145,10 @@ public class OssClient {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
+ public UploadResult uploadSuffix(File file, String suffix) {
+ return upload(file, getPath(properties.getPrefix(), suffix));
+ }
+
/**
* 获取文件元数据
*
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java b/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java
index 0442af0db..cf718e637 100644
--- a/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java
+++ b/ruoyi/src/main/java/com/ruoyi/sms/config/SmsConfig.java
@@ -1,45 +1,12 @@
package com.ruoyi.sms.config;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.AliyunSmsTemplate;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.core.TencentSmsTemplate;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
/**
* 短信配置类
*
* @author Lion Li
* @version 4.2.0
*/
-@Configuration
+//@Configuration // 暂时用不上 留着后续扩展使用
public class SmsConfig {
-// @Configuration
-// @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
-// @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
-// static class AliyunSmsConfig {
-//
-// @Bean
-// public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
-// return new AliyunSmsTemplate(smsProperties);
-// }
-//
-// }
-
- @Configuration
- @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
- @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
- static class TencentSmsConfig {
-
- @Bean
- public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
- return new TencentSmsTemplate(smsProperties);
- }
-
- }
-
}
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
index 39359cdfd..3f5e03621 100644
--- a/ruoyi/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
+++ b/ruoyi/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
@@ -1,47 +1,20 @@
-package com.ruoyi.sms.config.properties;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-/**
- * SMS短信 配置属性
- *
- * @author Lion Li
- * @version 4.2.0
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "sms")
-public class SmsProperties {
-
- private Boolean enabled;
-
- /**
- * 配置节点
- * 阿里云 dysmsapi.aliyuncs.com
- * 腾讯云 sms.tencentcloudapi.com
- */
- private String endpoint;
-
- /**
- * key
- */
- private String accessKeyId;
-
- /**
- * 密匙
- */
- private String accessKeySecret;
-
- /*
- * 短信签名
- */
- private String signName;
-
- /**
- * 短信应用ID (腾讯专属)
- */
- private String sdkAppId;
-
-}
+//package com.ruoyi.sms.config.properties;
+//
+//import lombok.Data;
+//import org.springframework.boot.context.properties.ConfigurationProperties;
+//import org.springframework.stereotype.Component;
+//
+///**
+// * SMS短信 配置属性
+// *
+// * @author Lion Li
+// * @version 4.2.0
+// */
+//@Data
+//@Component
+//@ConfigurationProperties(prefix = "sms")
+//public class SmsProperties {
+//
+// private Boolean enabled;
+//
+//}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java b/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
deleted file mode 100644
index 3c16a5b41..000000000
--- a/ruoyi/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.aliyun.dysmsapi20170525.Client;
-import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
-import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
-import com.aliyun.teaopenapi.models.Config;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import lombok.SneakyThrows;
-
-import java.util.Map;
-
-/**
- * Aliyun 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class AliyunSmsTemplate implements SmsTemplate {
-
- private SmsProperties properties;
-
- private Client client;
-
- @SneakyThrows(Exception.class)
- public AliyunSmsTemplate(SmsProperties smsProperties) {
- this.properties = smsProperties;
- Config config = new Config()
- // 您的AccessKey ID
- .setAccessKeyId(smsProperties.getAccessKeyId())
- // 您的AccessKey Secret
- .setAccessKeySecret(smsProperties.getAccessKeySecret())
- // 访问的域名
- .setEndpoint(smsProperties.getEndpoint());
- this.client = new Client(config);
- }
-
- @Override
- public SmsResult send(String phones, String templateId, Map param) {
- if (StringUtils.isBlank(phones)) {
- throw new SmsException("手机号不能为空");
- }
- if (StringUtils.isBlank(templateId)) {
- throw new SmsException("模板ID不能为空");
- }
- SendSmsRequest req = new SendSmsRequest()
- .setPhoneNumbers(phones)
- .setSignName(properties.getSignName())
- .setTemplateCode(templateId)
- .setTemplateParam(JsonUtils.toJsonString(param));
- try {
- SendSmsResponse resp = client.sendSms(req);
- return SmsResult.builder()
- .isSuccess("OK".equals(resp.getBody().getCode()))
- .message(resp.getBody().getMessage())
- .response(JsonUtils.toJsonString(resp))
- .build();
- } catch (Exception e) {
- throw new SmsException(e.getMessage());
- }
- }
-
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java b/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
deleted file mode 100644
index 0aec3ddbe..000000000
--- a/ruoyi/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.ruoyi.sms.entity.SmsResult;
-
-import java.util.Map;
-
-/**
- * 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public interface SmsTemplate {
-
- /**
- * 发送短信
- *
- * @param phones 电话号(多个逗号分割)
- * @param templateId 模板id
- * @param param 模板对应参数
- * 阿里 需使用 模板变量名称对应内容 例如: code=1234
- * 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
- */
- SmsResult send(String phones, String templateId, Map 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
deleted file mode 100644
index 69ec7f1ec..000000000
--- a/ruoyi/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.ruoyi.sms.core;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ArrayUtil;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import com.tencentcloudapi.common.Credential;
-import com.tencentcloudapi.common.profile.ClientProfile;
-import com.tencentcloudapi.common.profile.HttpProfile;
-import com.tencentcloudapi.sms.v20190711.SmsClient;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
-import com.tencentcloudapi.sms.v20190711.models.SendStatus;
-import lombok.SneakyThrows;
-
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Tencent 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class TencentSmsTemplate implements SmsTemplate {
-
- private SmsProperties properties;
-
- private SmsClient client;
-
- @SneakyThrows(Exception.class)
- public TencentSmsTemplate(SmsProperties smsProperties) {
- this.properties = smsProperties;
- Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
- HttpProfile httpProfile = new HttpProfile();
- httpProfile.setEndpoint(smsProperties.getEndpoint());
- ClientProfile clientProfile = new ClientProfile();
- clientProfile.setHttpProfile(httpProfile);
- this.client = new SmsClient(credential, "", clientProfile);
- }
-
- @Override
- public SmsResult send(String phones, String templateId, Map 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(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
- req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
- if (CollUtil.isNotEmpty(param)) {
- req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
- }
- req.setTemplateID(templateId);
- req.setSign(properties.getSignName());
- req.setSmsSdkAppid(properties.getSdkAppId());
- try {
- SendSmsResponse resp = client.SendSms(req);
- SmsResult.SmsResultBuilder builder = SmsResult.builder()
- .isSuccess(true)
- .message("send success")
- .response(JsonUtils.toJsonString(resp));
- for (SendStatus sendStatus : resp.getSendStatusSet()) {
- if (!"Ok".equals(sendStatus.getCode())) {
- builder.isSuccess(false).message(sendStatus.getMessage());
- break;
- }
- }
- return builder.build();
- } catch (Exception e) {
- throw new SmsException(e.getMessage());
- }
- }
-
-}
diff --git a/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java b/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java
deleted file mode 100644
index 89c39b403..000000000
--- a/ruoyi/src/main/java/com/ruoyi/sms/entity/SmsResult.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.ruoyi.sms.entity;
-
-import lombok.Builder;
-import lombok.Data;
-
-/**
- * 上传返回体
- *
- * @author Lion Li
- */
-@Data
-@Builder
-public class SmsResult {
-
- /**
- * 是否成功
- */
- private boolean isSuccess;
-
- /**
- * 响应消息
- */
- private String message;
-
- /**
- * 实际响应体
- *
- * 可自行转换为 SDK 对应的 SendSmsResponse
- */
- private String 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
deleted file mode 100644
index 28632a375..000000000
--- a/ruoyi/src/main/java/com/ruoyi/sms/exception/SmsException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.ruoyi.sms.exception;
-
-/**
- * Sms异常类
- *
- * @author Lion Li
- */
-public class SmsException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- public SmsException(String msg) {
- super(msg);
- }
-
-}
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 6472cebcb..f2221b107 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/ISysOssService.java
@@ -2,12 +2,12 @@ package com.ruoyi.system.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssVo;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
+import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@@ -27,6 +27,8 @@ public interface ISysOssService {
SysOssVo upload(MultipartFile file);
+ SysOssVo upload(File file);
+
void download(Long ossId, HttpServletResponse response) throws IOException;
Boolean deleteWithValidByIds(Collection ids, Boolean isValid);
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 8b0616471..1fa41b805 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/SysLoginService.java
@@ -8,9 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
-import com.ruoyi.common.core.domain.event.LogininforEvent;
import com.ruoyi.common.core.domain.dto.RoleDTO;
import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.event.LogininforEvent;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.model.XcxLoginUser;
import com.ruoyi.common.enums.DeviceType;
@@ -71,9 +71,10 @@ public class SysLoginService {
if (captchaEnabled) {
validateCaptcha(username, code, uuid);
}
+ // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUser user = loadUserByUsername(username);
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
@@ -88,7 +89,7 @@ public class SysLoginService {
SysUser user = loadUserByPhonenumber(phonenumber);
checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -99,11 +100,11 @@ public class SysLoginService {
}
public String emailLogin(String email, String emailCode) {
- // 通过手机号查找用户
+ // 通过手邮箱查找用户
SysUser user = loadUserByEmail(email);
checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -118,9 +119,11 @@ public class SysLoginService {
// todo 以下自行实现
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
String openid = "";
+
+ // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUser user = loadUserByOpenid(openid);
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName());
@@ -301,25 +304,24 @@ public class SysLoginService {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
- // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
- Integer errorNumber = RedisUtils.getCacheObject(errorKey);
+ // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
+ int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出
- if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
+ if (errorNumber >= maxRetryCount) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}
if (supplier.get()) {
- // 是否第一次
- errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
+ // 错误次数递增
+ errorNumber++;
+ RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录
- if (errorNumber.equals(maxRetryCount)) {
- RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+ if (errorNumber >= maxRetryCount) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
- // 未达到规定错误次数 则递增
- RedisUtils.setCacheObject(errorKey, errorNumber);
+ // 未达到规定错误次数
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
index 0c897df61..85d892411 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
@@ -116,7 +116,6 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
* @param dictType 字典类型
* @return 字典类型
*/
- @Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
@Override
public SysDictType selectDictTypeByType(String dictType) {
return baseMapper.selectById(new LambdaQueryWrapper().eq(SysDictType::getDictType, dictType));
@@ -148,7 +147,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
List dictDataList = dictDataMapper.selectList(
new LambdaQueryWrapper().eq(SysDictData::getStatus, UserConstants.DICT_NORMAL));
Map> dictDataMap = StreamUtils.groupByKey(dictDataList, SysDictData::getDictType);
- dictDataMap.forEach((k,v) -> {
+ dictDataMap.forEach((k, v) -> {
List dictList = StreamUtils.sorted(v, Comparator.comparing(SysDictData::getDictSort));
CacheUtils.put(CacheNames.SYS_DICT, k, dictList);
});
@@ -182,6 +181,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
public List insertDictType(SysDictType dict) {
int row = baseMapper.insert(dict);
if (row > 0) {
+ // 新增 type 下无 data 数据 返回空防止缓存穿透
return new ArrayList<>();
}
throw new ServiceException("操作失败");
@@ -279,4 +279,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
}
}
+ @Override
+ public Map getAllDictByDictType(String dictType) {
+ List list = selectDictDataByType(dictType);
+ return StreamUtils.toMap(list, SysDictData::getDictValue, SysDictData::getDictLabel);
+ }
}
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 c36bfa274..ce60d31a8 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.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -11,7 +12,6 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.service.OssService;
import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.utils.BeanCopyUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
@@ -31,9 +31,13 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -108,7 +112,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
}
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
- OssClient storage = OssFactory.instance();
+ OssClient storage = OssFactory.instance(sysOss.getService());
try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
int available = inputStream.available();
IoUtil.copy(inputStream, response.getOutputStream(), available);
@@ -130,15 +134,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
throw new ServiceException(e.getMessage());
}
// 保存文件信息
+ return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+ }
+
+ @Override
+ public SysOssVo upload(File file) {
+ String originalfileName = file.getName();
+ String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
+ OssClient storage = OssFactory.instance();
+ UploadResult uploadResult = storage.uploadSuffix(file, suffix);
+ // 保存文件信息
+ return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+ }
+
+ private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
SysOss oss = new SysOss();
oss.setUrl(uploadResult.getUrl());
oss.setFileSuffix(suffix);
oss.setFileName(uploadResult.getFilename());
oss.setOriginalName(originalfileName);
- oss.setService(storage.getConfigKey());
+ oss.setService(configKey);
baseMapper.insert(oss);
- SysOssVo sysOssVo = new SysOssVo();
- BeanCopyUtils.copy(oss, sysOssVo);
+ SysOssVo sysOssVo = BeanUtil.toBean(oss, SysOssVo.class);
return this.matchingUrl(sysOssVo);
}
diff --git a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
index b28df9e98..84e762175 100644
--- a/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
+++ b/ruoyi/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
@@ -186,6 +186,20 @@ public class SysRoleServiceImpl implements ISysRoleService {
if (ObjectUtil.isNotNull(role.getRoleId()) && role.isAdmin()) {
throw new ServiceException("不允许操作超级管理员角色");
}
+ // 新增不允许使用 管理员标识符
+ if (ObjectUtil.isNull(role.getRoleId())
+ && StringUtils.equals(role.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+ throw new ServiceException("不允许使用系统内置管理员角色标识符!");
+ }
+ // 修改不允许修改 管理员标识符
+ if (ObjectUtil.isNotNull(role.getRoleId())) {
+ SysRole sysRole = baseMapper.selectById(role.getRoleId());
+ // 如果标识符不相等 判断为修改了管理员标识符
+ if (!StringUtils.equals(sysRole.getRoleKey(), role.getRoleKey())
+ && StringUtils.equals(sysRole.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+ throw new ServiceException("不允许修改系统内置管理员角色标识符!");
+ }
+ }
}
/**
@@ -342,9 +356,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
@Transactional(rollbackFor = Exception.class)
public int deleteRoleByIds(Long[] roleIds) {
for (Long roleId : roleIds) {
- checkRoleAllowed(new SysRole(roleId));
- checkRoleDataScope(roleId);
SysRole role = selectRoleById(roleId);
+ checkRoleAllowed(role);
+ checkRoleDataScope(roleId);
if (countUserRoleByRoleId(roleId) > 0) {
throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
}
@@ -420,6 +434,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
@Override
public void cleanOnlineUserByRole(Long roleId) {
+ // 如果角色未绑定用户 直接返回
+ Long num = userRoleMapper.selectCount(new LambdaQueryWrapper().eq(SysUserRole::getRoleId, roleId));
+ if (num == 0) {
+ return;
+ }
List keys = StpUtil.searchTokenValue("", 0, -1, false);
if (CollUtil.isEmpty(keys)) {
return;
@@ -428,11 +447,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
keys.parallelStream().forEach(key -> {
String token = StringUtils.substringAfterLast(key, ":");
// 如果已经过期则跳过
- if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+ if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
return;
}
LoginUser loginUser = LoginHelper.getLoginUser(token);
- if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
+ if (ObjectUtil.isNotNull(loginUser) && loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
try {
StpUtil.logoutByTokenValue(token);
} catch (NotLoginException ignored) {
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 f5a603a43..604b5dfa1 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
@@ -16,12 +16,13 @@ import com.ruoyi.common.utils.reflect.ReflectUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.CaptchaProperties;
import com.ruoyi.framework.config.properties.MailProperties;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.entity.SmsResult;
import com.ruoyi.system.service.ISysConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
import java.time.Duration;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -47,7 +49,6 @@ import java.util.Map;
public class CaptchaController {
private final CaptchaProperties captchaProperties;
- private final SmsProperties smsProperties;
private final ISysConfigService configService;
private final MailProperties mailProperties;
@@ -58,21 +59,18 @@ public class CaptchaController {
*/
@GetMapping("/captchaSms")
public R smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
map.put("code", code);
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- SmsResult result = smsTemplate.send(phonenumber, templateId, map);
- if (!result.isSuccess()) {
- log.error("验证码短信发送异常 => {}", result);
- return R.fail(result.getMessage());
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+ SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
+ if (!"OK".equals(smsResponse.getCode())) {
+ log.error("验证码短信发送异常 => {}", smsResponse);
+ return R.fail(smsResponse.getMessage());
}
return R.ok();
}
diff --git a/ruoyi/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ruoyi/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
index 33d425a6a..17e6d5af5 100644
--- a/ruoyi/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
+++ b/ruoyi/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
@@ -47,7 +47,7 @@ public class SysUserOnlineController extends BaseController {
for (String key : keys) {
String token = StringUtils.substringAfterLast(key, ":");
// 如果已经过期则跳过
- if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+ if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
continue;
}
userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));
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 f8edb5d81..97d7b3c12 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
@@ -80,6 +80,7 @@ public class SysRoleController extends BaseController {
@Log(title = "角色管理", businessType = BusinessType.INSERT)
@PostMapping
public R add(@Validated @RequestBody SysRole role) {
+ roleService.checkRoleAllowed(role);
if (!roleService.checkRoleNameUnique(role)) {
return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
} else if (!roleService.checkRoleKeyUnique(role)) {
diff --git a/ruoyi/src/main/resources/application-dev.yml b/ruoyi/src/main/resources/application-dev.yml
index 32123a764..ffbc1c81a 100644
--- a/ruoyi/src/main/resources/application-dev.yml
+++ b/ruoyi/src/main/resources/application-dev.yml
@@ -158,14 +158,29 @@ mail:
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
sms:
- enabled: false
# 阿里云 dysmsapi.aliyuncs.com
- # 腾讯云 sms.tencentcloudapi.com
- endpoint: "dysmsapi.aliyuncs.com"
- accessKeyId: xxxxxxx
- accessKeySecret: xxxxxx
- signName: 测试
- # 腾讯专用
- sdkAppId:
+ alibaba:
+ #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+ requestUrl: dysmsapi.aliyuncs.com
+ #阿里云的accessKey
+ accessKeyId: xxxxxxx
+ #阿里云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ tencent:
+ #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+ requestUrl: sms.tencentcloudapi.com
+ #腾讯云的accessKey
+ accessKeyId: xxxxxxx
+ #腾讯云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ #短信sdkAppId
+ sdkAppId: appid
+ #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+ territory: ap-guangzhou
diff --git a/ruoyi/src/main/resources/application-prod.yml b/ruoyi/src/main/resources/application-prod.yml
index 85f6e935d..6292ceab6 100644
--- a/ruoyi/src/main/resources/application-prod.yml
+++ b/ruoyi/src/main/resources/application-prod.yml
@@ -161,14 +161,29 @@ mail:
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
sms:
- enabled: false
# 阿里云 dysmsapi.aliyuncs.com
- # 腾讯云 sms.tencentcloudapi.com
- endpoint: "dysmsapi.aliyuncs.com"
- accessKeyId: xxxxxxx
- accessKeySecret: xxxxxx
- signName: 测试
- # 腾讯专用
- sdkAppId:
+ alibaba:
+ #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+ requestUrl: dysmsapi.aliyuncs.com
+ #阿里云的accessKey
+ accessKeyId: xxxxxxx
+ #阿里云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ tencent:
+ #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+ requestUrl: sms.tencentcloudapi.com
+ #腾讯云的accessKey
+ accessKeyId: xxxxxxx
+ #腾讯云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ #短信sdkAppId
+ sdkAppId: appid
+ #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+ territory: ap-guangzhou
diff --git a/ruoyi/src/main/resources/application.yml b/ruoyi/src/main/resources/application.yml
index a6617bc6f..da4154526 100644
--- a/ruoyi/src/main/resources/application.yml
+++ b/ruoyi/src/main/resources/application.yml
@@ -104,8 +104,9 @@ sa-token:
token-name: Authorization
# token有效期 设为一天 (必定过期) 单位: 秒
timeout: 86400
- # token临时有效期 (指定时间无操作就过期) 单位: 秒
- activity-timeout: 1800
+ # 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义
+ # token最低活跃时间 (指定时间无操作就过期) 单位: 秒
+ active-timeout: 1800
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml
index 6e16c1310..4359e7469 100644
--- a/script/docker/docker-compose.yml
+++ b/script/docker/docker-compose.yml
@@ -100,7 +100,7 @@ services:
network_mode: "host"
ruoyi-server1:
- image: ruoyi/ruoyi-server:4.7.0
+ image: ruoyi/ruoyi-server:4.8.0
container_name: ruoyi-server1
environment:
# 时区上海
@@ -115,7 +115,7 @@ services:
network_mode: "host"
ruoyi-server2:
- image: "ruoyi/ruoyi-server:4.7.0"
+ image: "ruoyi/ruoyi-server:4.8.0"
container_name: ruoyi-server2
environment:
# 时区上海
@@ -130,7 +130,7 @@ services:
network_mode: "host"
ruoyi-monitor-admin:
- image: ruoyi/ruoyi-monitor-admin:4.7.0
+ image: ruoyi/ruoyi-monitor-admin:4.8.0
container_name: ruoyi-monitor-admin
environment:
# 时区上海
@@ -142,7 +142,7 @@ services:
network_mode: "host"
ruoyi-xxl-job-admin:
- image: ruoyi/ruoyi-xxl-job-admin:4.7.0
+ image: ruoyi/ruoyi-xxl-job-admin:4.8.0
container_name: ruoyi-xxl-job-admin
environment:
# 时区上海