mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2025-09-24 07:19:46 +08:00
🎃 发布 4.8.0 新增 sms4j 短信融合
This commit is contained in:
@ -32,7 +32,7 @@ public @interface EncryptField {
|
||||
String publicKey() default "";
|
||||
|
||||
/**
|
||||
* 公钥。RSA、SM2需要
|
||||
* 私钥。RSA、SM2需要
|
||||
*/
|
||||
String privateKey() default "";
|
||||
|
||||
|
@ -129,4 +129,9 @@ public interface UserConstants {
|
||||
*/
|
||||
Long ADMIN_ID = 1L;
|
||||
|
||||
/**
|
||||
* 管理员角色key
|
||||
*/
|
||||
String ADMIN_ROLE_KEY = "admin";
|
||||
|
||||
}
|
||||
|
@ -37,14 +37,36 @@ public class ExcelEnumConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
Object codeValue = cellData.getData();
|
||||
cellData.checkEmpty();
|
||||
// Excel中填入的是枚举中指定的描述
|
||||
Object textValue = null;
|
||||
switch (cellData.getType()) {
|
||||
case STRING:
|
||||
case DIRECT_STRING:
|
||||
case RICH_TEXT_STRING:
|
||||
textValue = cellData.getStringValue();
|
||||
break;
|
||||
case NUMBER:
|
||||
textValue = cellData.getNumberValue();
|
||||
break;
|
||||
case BOOLEAN:
|
||||
textValue = cellData.getBooleanValue();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("单元格类型异常!");
|
||||
}
|
||||
// 如果是空值
|
||||
if (ObjectUtil.isNull(codeValue)) {
|
||||
if (ObjectUtil.isNull(textValue)) {
|
||||
return null;
|
||||
}
|
||||
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
|
||||
String textValue = enumValueMap.get(codeValue);
|
||||
return Convert.convert(contentProperty.getField().getType(), textValue);
|
||||
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
|
||||
// 从Java输出至Excel是code转text
|
||||
// 因此从Excel转Java应该将text与code对调
|
||||
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
|
||||
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
|
||||
// 应该从text -> code中查找
|
||||
Object codeValue = enumTextToCodeMap.get(textValue);
|
||||
return Convert.convert(contentProperty.getField().getType(), codeValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -53,6 +53,7 @@ public class SysUser extends BaseEntity {
|
||||
* 用户昵称
|
||||
*/
|
||||
@Xss(message = "用户昵称不能包含脚本字符")
|
||||
@NotBlank(message = "用户昵称不能为空")
|
||||
@Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
|
||||
private String nickName;
|
||||
|
||||
|
@ -6,7 +6,7 @@ import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 短信登录对象
|
||||
* 邮箱登录对象
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.ruoyi.common.core.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 通用 字典服务
|
||||
*
|
||||
@ -54,4 +56,11 @@ public interface DictService {
|
||||
*/
|
||||
String getDictValue(String dictType, String dictLabel, String separator);
|
||||
|
||||
/**
|
||||
* 获取字典下所有的字典值与标签
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return dictValue为key,dictLabel为值组成的Map
|
||||
*/
|
||||
Map<String, String> getAllDictByDictType(String dictType);
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.metadata.Head;
|
||||
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
|
||||
import com.ruoyi.common.annotation.CellMerge;
|
||||
import com.ruoyi.common.utils.reflect.ReflectUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -24,91 +25,97 @@ import java.util.Map;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy {
|
||||
|
||||
private List<?> list;
|
||||
private boolean hasTitle;
|
||||
private final List<CellRangeAddress> cellList;
|
||||
private final boolean hasTitle;
|
||||
private int rowIndex;
|
||||
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
List<CellRangeAddress> cellList = handle(list, hasTitle);
|
||||
// judge the list is not null
|
||||
if (CollectionUtils.isNotEmpty(cellList)) {
|
||||
// the judge is necessary
|
||||
if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
|
||||
for (CellRangeAddress item : cellList) {
|
||||
sheet.addMergedRegion(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle) {
|
||||
this.hasTitle = hasTitle;
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
this.cellList = handle(list, hasTitle);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollectionUtils.isEmpty(list)) {
|
||||
return cellList;
|
||||
}
|
||||
Class<?> clazz = list.get(0).getClass();
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> 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<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
String name = field.getName();
|
||||
String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
|
||||
Method readMethod = clazz.getMethod(methodName);
|
||||
Object val = readMethod.invoke(list.get(i));
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
// judge the list is not null
|
||||
if (CollUtil.isNotEmpty(cellList)) {
|
||||
// the judge is necessary
|
||||
if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
|
||||
for (CellRangeAddress item : cellList) {
|
||||
sheet.addMergedRegion(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.equals(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;
|
||||
}
|
||||
@SneakyThrows
|
||||
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return cellList;
|
||||
}
|
||||
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class RepeatCell {
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> 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());
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object value;
|
||||
Map<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
|
||||
|
||||
private int current;
|
||||
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.equals(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;
|
||||
|
||||
}
|
||||
}
|
||||
|
149
ruoyi/src/main/java/com/ruoyi/common/excel/DropDownOptions.java
Normal file
149
ruoyi/src/main/java/com/ruoyi/common/excel/DropDownOptions.java
Normal file
@ -0,0 +1,149 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <h1>Excel下拉可选项</h1>
|
||||
* 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@SuppressWarnings("unused")
|
||||
public class DropDownOptions {
|
||||
/**
|
||||
* 一级下拉所在列index,从0开始算
|
||||
*/
|
||||
private int index = 0;
|
||||
/**
|
||||
* 二级下拉所在的index,从0开始算,不能与一级相同
|
||||
*/
|
||||
private int nextIndex = 0;
|
||||
/**
|
||||
* 一级下拉所包含的数据
|
||||
*/
|
||||
private List<String> options = new ArrayList<>();
|
||||
/**
|
||||
* 二级下拉所包含的数据Map
|
||||
* <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
|
||||
*/
|
||||
private Map<String, List<String>> nextOptions = new HashMap<>();
|
||||
/**
|
||||
* 分隔符
|
||||
*/
|
||||
private static final String DELIMITER = "_";
|
||||
|
||||
/**
|
||||
* 创建只有一级的下拉选
|
||||
*/
|
||||
public DropDownOptions(int index, List<String> options) {
|
||||
this.index = index;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>创建每个选项可选值</h2>
|
||||
* <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
|
||||
*
|
||||
* @param vars 可选值内包含的参数
|
||||
* @return 合规的可选值
|
||||
*/
|
||||
public static String createOptionValue(Object... vars) {
|
||||
StringBuilder stringBuffer = new StringBuilder();
|
||||
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
|
||||
for (int i = 0; i < vars.length; i++) {
|
||||
String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
|
||||
if (!var.matches(regex)) {
|
||||
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
|
||||
}
|
||||
stringBuffer.append(var);
|
||||
if (i < vars.length - 1) {
|
||||
// 直至最后一个前,都以_作为切割线
|
||||
stringBuffer.append(DELIMITER);
|
||||
}
|
||||
}
|
||||
if (stringBuffer.toString().matches("^\\d_*$")) {
|
||||
throw new ServiceException("禁止以数字开头");
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将处理后合理的可选值解析为原始的参数
|
||||
*
|
||||
* @param option 经过处理后的合理的可选项
|
||||
* @return 原始的参数
|
||||
*/
|
||||
public static List<String> analyzeOptionValue(String option) {
|
||||
return StrUtil.split(option, DELIMITER, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建级联下拉选项
|
||||
*
|
||||
* @param parentList 父实体可选项原始数据
|
||||
* @param parentIndex 父下拉选位置
|
||||
* @param sonList 子实体可选项原始数据
|
||||
* @param sonIndex 子下拉选位置
|
||||
* @param parentHowToGetIdFunction 父类如何获取唯一标识
|
||||
* @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
|
||||
* @param howToBuildEveryOption 如何生成下拉选内容
|
||||
* @return 级联下拉选项
|
||||
*/
|
||||
public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
|
||||
int parentIndex,
|
||||
List<T> sonList,
|
||||
int sonIndex,
|
||||
Function<T, Number> parentHowToGetIdFunction,
|
||||
Function<T, Number> sonHowToGetParentIdFunction,
|
||||
Function<T, String> howToBuildEveryOption) {
|
||||
DropDownOptions parentLinkSonOptions = new DropDownOptions();
|
||||
// 先创建父类的下拉
|
||||
parentLinkSonOptions.setIndex(parentIndex);
|
||||
parentLinkSonOptions.setOptions(
|
||||
parentList.stream()
|
||||
.map(howToBuildEveryOption)
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
// 提取父-子级联下拉
|
||||
Map<String, List<String>> sonOptions = new HashMap<>();
|
||||
// 父级依据自己的ID分组
|
||||
Map<Number, List<T>> parentGroupByIdMap =
|
||||
parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
|
||||
// 遍历每个子集,提取到Map中
|
||||
sonList.forEach(everySon -> {
|
||||
if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
|
||||
// 找到对应的上级
|
||||
T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
|
||||
// 提取名称和ID作为Key
|
||||
String key = howToBuildEveryOption.apply(parentObj);
|
||||
// Key对应的Value
|
||||
List<String> thisParentSonOptionList;
|
||||
if (sonOptions.containsKey(key)) {
|
||||
thisParentSonOptionList = sonOptions.get(key);
|
||||
} else {
|
||||
thisParentSonOptionList = new ArrayList<>();
|
||||
sonOptions.put(key, thisParentSonOptionList);
|
||||
}
|
||||
// 往Value中添加当前子集选项
|
||||
thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
|
||||
}
|
||||
});
|
||||
parentLinkSonOptions.setNextIndex(sonIndex);
|
||||
parentLinkSonOptions.setNextOptions(sonOptions);
|
||||
return parentLinkSonOptions;
|
||||
}
|
||||
}
|
370
ruoyi/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java
Normal file
370
ruoyi/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java
Normal file
@ -0,0 +1,370 @@
|
||||
package com.ruoyi.common.excel;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.EnumUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.write.handler.SheetWriteHandler;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.annotation.ExcelEnumFormat;
|
||||
import com.ruoyi.common.core.service.DictService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.ss.util.WorkbookUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <h1>Excel表格下拉选操作</h1>
|
||||
* 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
|
||||
* <p>
|
||||
* 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Slf4j
|
||||
public class ExcelDownHandler implements SheetWriteHandler {
|
||||
|
||||
/**
|
||||
* Excel表格中的列名英文
|
||||
* 仅为了解析列英文,禁止修改
|
||||
*/
|
||||
private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/**
|
||||
* 单选数据Sheet名
|
||||
*/
|
||||
private static final String OPTIONS_SHEET_NAME = "options";
|
||||
/**
|
||||
* 联动选择数据Sheet名的头
|
||||
*/
|
||||
private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
|
||||
/**
|
||||
* 下拉可选项
|
||||
*/
|
||||
private final List<DropDownOptions> dropDownOptions;
|
||||
/**
|
||||
* 当前单选进度
|
||||
*/
|
||||
private int currentOptionsColumnIndex;
|
||||
/**
|
||||
* 当前联动选择进度
|
||||
*/
|
||||
private int currentLinkedOptionsSheetIndex;
|
||||
private final DictService dictService;
|
||||
|
||||
public ExcelDownHandler(List<DropDownOptions> options) {
|
||||
this.dropDownOptions = options;
|
||||
this.currentOptionsColumnIndex = 0;
|
||||
this.currentLinkedOptionsSheetIndex = 0;
|
||||
this.dictService = SpringUtils.getBean(DictService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>开始创建下拉数据</h2>
|
||||
* 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
|
||||
* 如果有且设置了value值,则将其直接置为下拉可选项
|
||||
* <p>
|
||||
* 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
|
||||
* <p>
|
||||
* 3.二者并存,注意调用方式
|
||||
*/
|
||||
@Override
|
||||
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
|
||||
Sheet sheet = writeSheetHolder.getSheet();
|
||||
// 开始设置下拉框 HSSFWorkbook
|
||||
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||
Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
|
||||
Workbook workbook = writeWorkbookHolder.getWorkbook();
|
||||
int length = fields.length;
|
||||
for (int i = 0; i < length; i++) {
|
||||
// 循环实体中的每个属性
|
||||
// 可选的下拉值
|
||||
List<String> options = new ArrayList<>();
|
||||
if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
|
||||
// 如果指定了@ExcelDictFormat,则使用字典的逻辑
|
||||
ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
|
||||
String dictType = format.dictType();
|
||||
String converterExp = format.readConverterExp();
|
||||
if (StrUtil.isNotBlank(dictType)) {
|
||||
// 如果传递了字典名,则依据字典建立下拉
|
||||
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
|
||||
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
|
||||
.values();
|
||||
options = new ArrayList<>(values);
|
||||
} else if (StrUtil.isNotBlank(converterExp)) {
|
||||
// 如果指定了确切的值,则直接解析确切的值
|
||||
options = StrUtil.split(converterExp, format.separator(), true, true);
|
||||
}
|
||||
} else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
|
||||
// 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
|
||||
ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||
options = StreamUtils.toList(values, String::valueOf);
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(options)) {
|
||||
// 仅当下拉可选项不为空时执行
|
||||
// 获取列下标,默认为当前循环次数
|
||||
int index = i;
|
||||
if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
|
||||
// 如果指定了列下标,以指定的为主
|
||||
index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
|
||||
}
|
||||
if (options.size() > 20) {
|
||||
// 这里限制如果可选项大于20,则使用额外表形式
|
||||
dropDownWithSheet(helper, workbook, sheet, index, options);
|
||||
} else {
|
||||
// 否则使用固定值形式
|
||||
dropDownWithSimple(helper, sheet, index, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
dropDownOptions.forEach(everyOptions -> {
|
||||
// 如果传递了下拉框选择器参数
|
||||
if (!everyOptions.getNextOptions().isEmpty()) {
|
||||
// 当二级选项不为空时,使用额外关联表的形式
|
||||
dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
|
||||
} else if (everyOptions.getOptions().size() > 10) {
|
||||
// 当一级选项参数个数大于10,使用额外表的形式
|
||||
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
} else if (everyOptions.getOptions().size() != 0) {
|
||||
// 当一级选项个数不为空,使用默认形式
|
||||
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>简单下拉框</h2>
|
||||
* 直接将可选项拼接为指定列的数据校验值
|
||||
*
|
||||
* @param celIndex 列index
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
if (ObjectUtil.isEmpty(value)) {
|
||||
return;
|
||||
}
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的级联下拉框</h2>
|
||||
*
|
||||
* @param options 额外表格形式存储的下拉可选项
|
||||
*/
|
||||
private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
|
||||
String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
|
||||
// 创建联动下拉数据表
|
||||
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
|
||||
// 完善横向的一级选项数据表
|
||||
List<String> firstOptions = options.getOptions();
|
||||
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
name.setNameName(linkedOptionsSheetName);
|
||||
// 以横向第一行创建一级下拉拼接引用位置
|
||||
String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
|
||||
linkedOptionsSheetName,
|
||||
getExcelColumnName(0),
|
||||
getExcelColumnName(firstOptions.size())
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(firstOptionsFunction);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
|
||||
|
||||
for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
|
||||
// 先提取主表中一级下拉的列名
|
||||
String firstOptionsColumnName = getExcelColumnName(columIndex);
|
||||
// 一次循环是每一个一级选项
|
||||
int finalI = columIndex;
|
||||
// 本次循环的一级选项值
|
||||
String thisFirstOptionsValue = firstOptions.get(columIndex);
|
||||
// 创建第一行的数据
|
||||
Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
|
||||
// 如果不存在则创建第一行
|
||||
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
|
||||
// 第一行当前列
|
||||
.createCell(columIndex)
|
||||
// 设置值为当前一级选项值
|
||||
.setCellValue(thisFirstOptionsValue);
|
||||
|
||||
// 第二行开始,设置第二级别选项参数
|
||||
List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
|
||||
if (CollUtil.isEmpty(secondOptions)) {
|
||||
// 必须保证至少有一个关联选项,否则将导致Excel解析错误
|
||||
secondOptions = Collections.singletonList("暂无_0");
|
||||
}
|
||||
|
||||
// 以该一级选项值创建子名称管理器
|
||||
Name sonName = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
sonName.setNameName(thisFirstOptionsValue);
|
||||
// 以第二行该列数据拼接引用位置
|
||||
String sonFunction = String.format("%s!$%s$2:$%s$%d",
|
||||
linkedOptionsSheetName,
|
||||
firstOptionsColumnName,
|
||||
firstOptionsColumnName,
|
||||
secondOptions.size() + 1
|
||||
);
|
||||
// 设置名称管理器的引用位置
|
||||
sonName.setRefersToFormula(sonFunction);
|
||||
// 数据验证为序列模式,引用到每一个主表中的二级选项位置
|
||||
// 创建子项的名称管理器,只是为了使得Excel可以识别到数据
|
||||
String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
|
||||
for (int i = 0; i < 100; i++) {
|
||||
// 以一级选项对应的主体所在位置创建二级下拉
|
||||
String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
|
||||
// 二级只能主表每一行的每一列添加二级校验
|
||||
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
|
||||
}
|
||||
|
||||
for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
|
||||
// 从第二行开始填充二级选项
|
||||
int finalRowIndex = rowIndex + 1;
|
||||
int finalColumIndex = columIndex;
|
||||
|
||||
Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
|
||||
// 没有则创建
|
||||
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
|
||||
Optional
|
||||
// 在本级一级选项所在的列
|
||||
.ofNullable(row.getCell(finalColumIndex))
|
||||
// 不存在则创建
|
||||
.orElseGet(() -> row.createCell(finalColumIndex))
|
||||
// 设置二级选项值
|
||||
.setCellValue(secondOptions.get(rowIndex));
|
||||
}
|
||||
}
|
||||
|
||||
currentLinkedOptionsSheetIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>额外表格形式的普通下拉框</h2>
|
||||
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
|
||||
*
|
||||
* @param celIndex 下拉选
|
||||
* @param value 下拉选可选值
|
||||
*/
|
||||
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
|
||||
// 创建下拉数据表
|
||||
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
|
||||
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
|
||||
// 将下拉表隐藏
|
||||
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
|
||||
// 完善纵向的一级选项数据表
|
||||
for (int i = 0; i < value.size(); i++) {
|
||||
int finalI = i;
|
||||
// 获取每一选项行,如果没有则创建
|
||||
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
|
||||
.orElseGet(() -> simpleDataSheet.createRow(finalI));
|
||||
// 获取本级选项对应的选项列,如果没有则创建
|
||||
Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
|
||||
.orElseGet(() -> row.createCell(currentOptionsColumnIndex));
|
||||
// 设置值
|
||||
cell.setCellValue(value.get(i));
|
||||
}
|
||||
|
||||
// 创建名称管理器
|
||||
Name name = workbook.createName();
|
||||
// 设置名称管理器的别名
|
||||
String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
|
||||
name.setNameName(nameName);
|
||||
// 以纵向第一列创建一级下拉拼接引用位置
|
||||
String function = String.format("%s!$%s$1:$%s$%d",
|
||||
OPTIONS_SHEET_NAME,
|
||||
getExcelColumnName(currentOptionsColumnIndex),
|
||||
getExcelColumnName(currentOptionsColumnIndex),
|
||||
value.size());
|
||||
// 设置名称管理器的引用位置
|
||||
name.setRefersToFormula(function);
|
||||
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||
this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
|
||||
currentOptionsColumnIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限一级选项
|
||||
*/
|
||||
private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
|
||||
DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载下拉的列,仅限二级选项
|
||||
*/
|
||||
private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
|
||||
Integer celIndex, DataValidationConstraint constraint) {
|
||||
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||
CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
|
||||
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据校验
|
||||
*/
|
||||
private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
|
||||
DataValidationConstraint constraint, CellRangeAddressList addressList) {
|
||||
// 数据有效性对象
|
||||
DataValidation dataValidation = helper.createValidation(constraint, addressList);
|
||||
// 处理Excel兼容性问题
|
||||
if (dataValidation instanceof XSSFDataValidation) {
|
||||
//数据校验
|
||||
dataValidation.setSuppressDropDownArrow(true);
|
||||
//错误提示
|
||||
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
|
||||
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
|
||||
dataValidation.setShowErrorBox(true);
|
||||
//选定提示
|
||||
dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
|
||||
dataValidation.setShowPromptBox(true);
|
||||
sheet.addValidationData(dataValidation);
|
||||
} else {
|
||||
dataValidation.setSuppressDropDownArrow(false);
|
||||
}
|
||||
sheet.addValidationData(dataValidation);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>依据列index获取列名英文</h2>
|
||||
* 依据列index转换为Excel中的列名英文
|
||||
* <p>例如第1列,index为0,解析出来为A列</p>
|
||||
* 第27列,index为26,解析为AA列
|
||||
* <p>第28列,index为27,解析为AB列</p>
|
||||
*
|
||||
* @param columnIndex 列index
|
||||
* @return 列index所在得英文名
|
||||
*/
|
||||
private String getExcelColumnName(int columnIndex) {
|
||||
// 26一循环的次数
|
||||
int columnCircleCount = columnIndex / 26;
|
||||
// 26一循环内的位置
|
||||
int thisCircleColumnIndex = columnIndex % 26;
|
||||
// 26一循环的次数大于0,则视为栏名至少两位
|
||||
String columnPrefix = columnCircleCount == 0
|
||||
? StrUtil.EMPTY
|
||||
: StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
|
||||
// 从26一循环内取对应的栏位名
|
||||
String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
|
||||
// 将二者拼接即为最终的栏位名
|
||||
return columnPrefix + columnNext;
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.ruoyi.common.helper;
|
||||
|
||||
import cn.dev33.satoken.context.SaHolder;
|
||||
import cn.dev33.satoken.context.model.SaStorage;
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
@ -54,6 +55,14 @@ public class LoginHelper {
|
||||
if (ObjectUtil.isNotNull(deviceType)) {
|
||||
model.setDevice(deviceType.getDevice());
|
||||
}
|
||||
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||
// UserType userType = UserType.getUserType(loginUser.getUserType());
|
||||
// if (userType == UserType.SYS_USER) {
|
||||
// model.setTimeout(86400).setActiveTimeout(1800);
|
||||
// } else if (userType == UserType.APP_USER) {
|
||||
// model.setTimeout(86400).setActiveTimeout(1800);
|
||||
// }
|
||||
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
|
||||
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
|
||||
}
|
||||
@ -66,7 +75,11 @@ public class LoginHelper {
|
||||
if (loginUser != null) {
|
||||
return loginUser;
|
||||
}
|
||||
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
|
||||
SaSession session = StpUtil.getTokenSession();
|
||||
if (ObjectUtil.isNull(session)) {
|
||||
return null;
|
||||
}
|
||||
loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
|
||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||
return loginUser;
|
||||
}
|
||||
@ -75,7 +88,11 @@ public class LoginHelper {
|
||||
* 获取用户基于token
|
||||
*/
|
||||
public static LoginUser getLoginUser(String token) {
|
||||
return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
|
||||
SaSession session = StpUtil.getTokenSessionByToken(token);
|
||||
if (ObjectUtil.isNull(session)) {
|
||||
return null;
|
||||
}
|
||||
return (LoginUser) session.get(LOGIN_USER_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,8 +130,8 @@ public class LoginHelper {
|
||||
* 获取用户类型
|
||||
*/
|
||||
public static UserType getUserType() {
|
||||
String loginId = StpUtil.getLoginIdAsString();
|
||||
return UserType.getUserType(loginId);
|
||||
String loginType = StpUtil.getLoginIdAsString();
|
||||
return UserType.getUserType(loginType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,11 +17,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* bean深拷贝工具(基于 cglib 性能优异)
|
||||
* bean拷贝工具(基于 cglib 性能优异)
|
||||
* <p>
|
||||
* 重点 cglib 不支持 拷贝到链式对象
|
||||
* 例如: 源对象 拷贝到 目标(链式对象)
|
||||
* 请区分好`浅拷贝`和`深拷贝`再做使用
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
@ -30,6 +30,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(function).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -70,7 +71,8 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return collection.stream().sorted(comparing).collect(Collectors.toList());
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,7 +89,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +108,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,7 +126,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
@ -145,7 +147,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
|
||||
}
|
||||
|
||||
@ -166,7 +168,7 @@ public class StreamUtils {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
.stream().filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
|
||||
}
|
||||
|
||||
@ -188,6 +190,7 @@ public class StreamUtils {
|
||||
.stream()
|
||||
.map(function)
|
||||
.filter(Objects::nonNull)
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig;
|
||||
import com.alibaba.excel.write.metadata.fill.FillWrapper;
|
||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import com.ruoyi.common.convert.ExcelBigNumberConvert;
|
||||
import com.ruoyi.common.excel.CellMergeStrategy;
|
||||
import com.ruoyi.common.excel.DefaultExcelListener;
|
||||
import com.ruoyi.common.excel.ExcelListener;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.excel.*;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import lombok.AccessLevel;
|
||||
@ -88,7 +85,26 @@ public class ExcelUtil {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os);
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
@ -107,7 +123,27 @@ public class ExcelUtil {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os);
|
||||
exportExcel(list, sheetName, clazz, merge, os, null);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param merge 是否合并单元格
|
||||
* @param response 响应体
|
||||
* @param options 级联下拉选
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
|
||||
try {
|
||||
resetResponse(sheetName, response);
|
||||
ServletOutputStream os = response.getOutputStream();
|
||||
exportExcel(list, sheetName, clazz, merge, os, options);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("导出Excel异常");
|
||||
}
|
||||
@ -122,7 +158,20 @@ public class ExcelUtil {
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
|
||||
exportExcel(list, sheetName, clazz, false, os);
|
||||
exportExcel(list, sheetName, clazz, false, os, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param list 导出数据集合
|
||||
* @param sheetName 工作表的名称
|
||||
* @param clazz 实体类
|
||||
* @param os 输出流
|
||||
* @param options 级联下拉选内容
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
|
||||
exportExcel(list, sheetName, clazz, false, os, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,7 +183,8 @@ public class ExcelUtil {
|
||||
* @param merge 是否合并单元格
|
||||
* @param os 输出流
|
||||
*/
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
|
||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
|
||||
OutputStream os, List<DropDownOptions> options) {
|
||||
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
|
||||
.autoCloseStream(false)
|
||||
// 自动适配
|
||||
@ -146,6 +196,10 @@ public class ExcelUtil {
|
||||
// 合并处理器
|
||||
builder.registerWriteHandler(new CellMergeStrategy(list, true));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(options)) {
|
||||
// 添加下拉框操作
|
||||
builder.registerWriteHandler(new ExcelDownHandler(options));
|
||||
}
|
||||
builder.doWrite(list);
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,18 @@ public class RedisUtils {
|
||||
batch.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果不存在则设置 并返回 true 如果存在则返回 false
|
||||
*
|
||||
* @param key 缓存的键值
|
||||
* @param value 缓存的值
|
||||
* @return set成功或失败
|
||||
*/
|
||||
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
|
||||
RBucket<T> bucket = CLIENT.getBucket(key);
|
||||
return bucket.setIfAbsent(value, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册对象监听器
|
||||
* <p>
|
||||
@ -374,6 +386,21 @@ public class RedisUtils {
|
||||
return rMap.remove(hKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Hash中的数据
|
||||
*
|
||||
* @param key Redis键
|
||||
* @param hKeys Hash键
|
||||
*/
|
||||
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
|
||||
RBatch batch = CLIENT.createBatch();
|
||||
RMapAsync<String, T> rMap = batch.getMap(key);
|
||||
for (String hKey : hKeys) {
|
||||
rMap.removeAsync(hKey);
|
||||
}
|
||||
batch.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个Hash中的数据
|
||||
*
|
||||
|
@ -1,17 +1,17 @@
|
||||
package com.ruoyi.demo.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.sms4j.provider.enumerate.SupplierType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 短信演示案例
|
||||
@ -26,10 +26,6 @@ import java.util.Map;
|
||||
@RequestMapping("/demo/sms")
|
||||
public class SmsController {
|
||||
|
||||
private final SmsProperties smsProperties;
|
||||
// private final SmsTemplate smsTemplate; // 可以使用spring注入
|
||||
// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
|
||||
|
||||
/**
|
||||
* 发送短信Aliyun
|
||||
*
|
||||
@ -38,17 +34,11 @@ public class SmsController {
|
||||
*/
|
||||
@GetMapping("/sendAliyun")
|
||||
public R<Object> sendAliyun(String phones, String templateId) {
|
||||
if (!smsProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启短信功能!");
|
||||
}
|
||||
if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
|
||||
return R.fail("阿里云依赖未引入!");
|
||||
}
|
||||
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
|
||||
Map<String, String> map = new HashMap<>(1);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", "1234");
|
||||
Object send = smsTemplate.send(phones, templateId, map);
|
||||
return R.ok(send);
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
|
||||
return R.ok(smsResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,18 +49,12 @@ public class SmsController {
|
||||
*/
|
||||
@GetMapping("/sendTencent")
|
||||
public R<Object> sendTencent(String phones, String templateId) {
|
||||
if (!smsProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启短信功能!");
|
||||
}
|
||||
if (!SpringUtils.containsBean("tencentSmsTemplate")) {
|
||||
return R.fail("腾讯云依赖未引入!");
|
||||
}
|
||||
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
|
||||
Map<String, String> map = new HashMap<>(1);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
// map.put("2", "测试测试");
|
||||
map.put("1", "1234");
|
||||
Object send = smsTemplate.send(phones, templateId, map);
|
||||
return R.ok(send);
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
|
||||
return R.ok(smsResponse);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
package com.ruoyi.demo.controller;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.ruoyi.common.excel.ExcelResult;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
import com.ruoyi.demo.listener.ExportDemoListener;
|
||||
import com.ruoyi.demo.service.IExportExcelService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
@ -20,9 +25,12 @@ import java.util.Map;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/demo/excel")
|
||||
public class TestExcelController {
|
||||
|
||||
private final IExportExcelService exportExcelService;
|
||||
|
||||
/**
|
||||
* 单列表多数据
|
||||
*/
|
||||
@ -76,6 +84,26 @@ public class TestExcelController {
|
||||
ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出下拉框
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
@GetMapping("/exportWithOptions")
|
||||
public void exportWithOptions(HttpServletResponse response) {
|
||||
exportExcelService.exportWithOptions(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入表格
|
||||
*/
|
||||
@PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public List<ExportDemoVo> importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
|
||||
// 处理解析结果
|
||||
ExcelResult<ExportDemoVo> excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
|
||||
return excelResult.getList();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
static class TestObj1 {
|
||||
|
119
ruoyi/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
Normal file
119
ruoyi/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
Normal file
@ -0,0 +1,119 @@
|
||||
package com.ruoyi.demo.domain.vo;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.ruoyi.common.annotation.ExcelDictFormat;
|
||||
import com.ruoyi.common.annotation.ExcelEnumFormat;
|
||||
import com.ruoyi.common.convert.ExcelDictConvert;
|
||||
import com.ruoyi.common.convert.ExcelEnumConvert;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 带有下拉选的Excel导出
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ExportDemoVo {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
@ExcelProperty(value = "用户名", index = 0)
|
||||
@NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
* </p>
|
||||
* 使用ExcelEnumFormat注解需要进行下拉选的部分
|
||||
*/
|
||||
@ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
|
||||
@ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
|
||||
@NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
|
||||
private String userStatus;
|
||||
|
||||
/**
|
||||
* 性别
|
||||
* <p>
|
||||
* 使用ExcelDictFormat注解需要进行下拉选的部分
|
||||
*/
|
||||
@ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_user_sex")
|
||||
@NotEmpty(message = "性别不能为空", groups = AddGroup.class)
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@ExcelProperty(value = "手机号", index = 3)
|
||||
@NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* Email
|
||||
*/
|
||||
@ExcelProperty(value = "Email", index = 4)
|
||||
@NotEmpty(message = "Email不能为空", groups = AddGroup.class)
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 省
|
||||
* <p>
|
||||
* 级联下拉,仅判断是否选了
|
||||
*/
|
||||
@ExcelProperty(value = "省", index = 5)
|
||||
@NotNull(message = "省不能为空", groups = AddGroup.class)
|
||||
private String province;
|
||||
|
||||
/**
|
||||
* 数据库中的省ID
|
||||
* </p>
|
||||
* 处理完毕后再判断是否市正确的值
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer provinceId;
|
||||
|
||||
/**
|
||||
* 市
|
||||
* <p>
|
||||
* 级联下拉
|
||||
*/
|
||||
@ExcelProperty(value = "市", index = 6)
|
||||
@NotNull(message = "市不能为空", groups = AddGroup.class)
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 数据库中的市ID
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer cityId;
|
||||
|
||||
/**
|
||||
* 县
|
||||
* <p>
|
||||
* 级联下拉
|
||||
*/
|
||||
@ExcelProperty(value = "县", index = 7)
|
||||
@NotNull(message = "县不能为空", groups = AddGroup.class)
|
||||
private String area;
|
||||
|
||||
/**
|
||||
* 数据库中的县ID
|
||||
*/
|
||||
@NotNull(message = "请勿手动输入", groups = EditGroup.class)
|
||||
private Integer areaId;
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.ruoyi.demo.listener;
|
||||
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.ruoyi.common.core.validate.AddGroup;
|
||||
import com.ruoyi.common.core.validate.EditGroup;
|
||||
import com.ruoyi.common.excel.DefaultExcelListener;
|
||||
import com.ruoyi.common.excel.DropDownOptions;
|
||||
import com.ruoyi.common.utils.ValidatorUtils;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Excel带下拉框的解析处理器
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
|
||||
|
||||
public ExportDemoListener() {
|
||||
// 显示使用构造函数,否则将导致空指针
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invoke(ExportDemoVo data, AnalysisContext context) {
|
||||
// 先校验必填
|
||||
ValidatorUtils.validate(data, AddGroup.class);
|
||||
|
||||
// 处理级联下拉的部分
|
||||
String province = data.getProvince();
|
||||
String city = data.getCity();
|
||||
String area = data.getArea();
|
||||
// 本行用户选择的省
|
||||
List<String> thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
|
||||
if (thisRowSelectedProvinceOption.size() == 2) {
|
||||
String provinceIdStr = thisRowSelectedProvinceOption.get(1);
|
||||
if (NumberUtil.isNumber(provinceIdStr)) {
|
||||
// 严格要求数据的话可以在这里做与数据库相关的判断
|
||||
// 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
|
||||
data.setProvinceId(Integer.parseInt(provinceIdStr));
|
||||
}
|
||||
}
|
||||
// 本行用户选择的市
|
||||
List<String> thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
|
||||
if (thisRowSelectedCityOption.size() == 2) {
|
||||
String cityIdStr = thisRowSelectedCityOption.get(1);
|
||||
if (NumberUtil.isNumber(cityIdStr)) {
|
||||
data.setCityId(Integer.parseInt(cityIdStr));
|
||||
}
|
||||
}
|
||||
// 本行用户选择的县
|
||||
List<String> thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
|
||||
if (thisRowSelectedAreaOption.size() == 2) {
|
||||
String areaIdStr = thisRowSelectedAreaOption.get(1);
|
||||
if (NumberUtil.isNumber(areaIdStr)) {
|
||||
data.setAreaId(Integer.parseInt(areaIdStr));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理完毕以后判断是否符合规则
|
||||
ValidatorUtils.validate(data, EditGroup.class);
|
||||
|
||||
// 添加到处理结果中
|
||||
getExcelResult().getList().add(data);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.ruoyi.demo.service;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 导出下拉框Excel示例
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
public interface IExportExcelService {
|
||||
|
||||
/**
|
||||
* 导出下拉框
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
void exportWithOptions(HttpServletResponse response);
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
package com.ruoyi.demo.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ruoyi.common.enums.UserStatus;
|
||||
import com.ruoyi.common.excel.DropDownOptions;
|
||||
import com.ruoyi.common.utils.StreamUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.demo.domain.vo.ExportDemoVo;
|
||||
import com.ruoyi.demo.service.IExportExcelService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 导出下拉框Excel示例
|
||||
*
|
||||
* @author Emil.Zhang
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
|
||||
@Override
|
||||
public void exportWithOptions(HttpServletResponse response) {
|
||||
// 创建表格数据,业务中一般通过数据库查询
|
||||
List<ExportDemoVo> excelDataList = new ArrayList<>();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// 模拟数据库中的一条数据
|
||||
ExportDemoVo everyRowData = new ExportDemoVo();
|
||||
everyRowData.setNickName("用户-" + i);
|
||||
everyRowData.setUserStatus(UserStatus.OK.getCode());
|
||||
everyRowData.setGender("1");
|
||||
everyRowData.setPhoneNumber(String.format("175%08d", i));
|
||||
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
|
||||
everyRowData.setProvinceId(i);
|
||||
everyRowData.setCityId(i);
|
||||
everyRowData.setAreaId(i);
|
||||
excelDataList.add(everyRowData);
|
||||
}
|
||||
|
||||
// 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
|
||||
// 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
|
||||
// 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
|
||||
|
||||
// 首先从数据库中查询下拉框内的可选项
|
||||
// 这里模拟查询结果
|
||||
List<DemoCityData> provinceList = getProvinceList(),
|
||||
cityList = getCityList(provinceList),
|
||||
areaList = getAreaList(cityList);
|
||||
int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
|
||||
|
||||
DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
|
||||
provinceList,
|
||||
provinceIndex,
|
||||
cityList,
|
||||
cityIndex,
|
||||
DemoCityData::getId,
|
||||
DemoCityData::getPid,
|
||||
everyOptions -> DropDownOptions.createOptionValue(
|
||||
everyOptions.getName(),
|
||||
everyOptions.getId()
|
||||
)
|
||||
);
|
||||
|
||||
DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
|
||||
cityList,
|
||||
cityIndex,
|
||||
areaList,
|
||||
areaIndex,
|
||||
DemoCityData::getId,
|
||||
DemoCityData::getPid,
|
||||
everyOptions -> DropDownOptions.createOptionValue(
|
||||
everyOptions.getName(),
|
||||
everyOptions.getId()
|
||||
)
|
||||
);
|
||||
|
||||
// 把所有的下拉框存储
|
||||
List<DropDownOptions> options = new ArrayList<>();
|
||||
options.add(provinceToCity);
|
||||
options.add(cityToArea);
|
||||
|
||||
// 到此为止所有的下拉框可选项已全部配置完毕
|
||||
|
||||
// 接下来需要将Excel中的展示数据转换为对应的下拉选
|
||||
List<ExportDemoVo> outList = StreamUtils.toList(excelDataList, everyRowData -> {
|
||||
// 只需要处理没有使用@ExcelDictFormat注解的下拉框
|
||||
// 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
|
||||
everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
|
||||
everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
|
||||
everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
|
||||
return everyRowData;
|
||||
});
|
||||
|
||||
ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
|
||||
}
|
||||
|
||||
private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
|
||||
Map<Integer, List<DemoCityData>> groupByIdMap =
|
||||
cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
|
||||
if (groupByIdMap.containsKey(id)) {
|
||||
DemoCityData demoCityData = groupByIdMap.get(id).get(0);
|
||||
return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
|
||||
} else {
|
||||
return StrUtil.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查询数据库操作
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getProvinceList() {
|
||||
List<DemoCityData> provinceList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
provinceList.add(new DemoCityData(0, null, "安徽省"));
|
||||
provinceList.add(new DemoCityData(1, null, "江苏省"));
|
||||
|
||||
return provinceList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查找数据库操作,需要连带查询出省的数据
|
||||
*
|
||||
* @param provinceList 模拟的父省数据
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getCityList(List<DemoCityData> provinceList) {
|
||||
List<DemoCityData> cityList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
cityList.add(new DemoCityData(0, 0, "合肥市"));
|
||||
cityList.add(new DemoCityData(1, 0, "芜湖市"));
|
||||
cityList.add(new DemoCityData(2, 1, "南京市"));
|
||||
cityList.add(new DemoCityData(3, 1, "无锡市"));
|
||||
cityList.add(new DemoCityData(4, 1, "徐州市"));
|
||||
|
||||
selectParentData(provinceList, cityList);
|
||||
|
||||
return cityList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟查找数据库操作,需要连带查询出市的数据
|
||||
*
|
||||
* @param cityList 模拟的父市数据
|
||||
* @return /
|
||||
*/
|
||||
private List<DemoCityData> getAreaList(List<DemoCityData> cityList) {
|
||||
List<DemoCityData> areaList = new ArrayList<>();
|
||||
|
||||
// 实际业务中一般采用数据库读取的形式,这里直接拼接创建
|
||||
areaList.add(new DemoCityData(0, 0, "瑶海区"));
|
||||
areaList.add(new DemoCityData(1, 0, "庐江区"));
|
||||
areaList.add(new DemoCityData(2, 1, "南宁县"));
|
||||
areaList.add(new DemoCityData(3, 1, "镜湖区"));
|
||||
areaList.add(new DemoCityData(4, 2, "玄武区"));
|
||||
areaList.add(new DemoCityData(5, 2, "秦淮区"));
|
||||
areaList.add(new DemoCityData(6, 3, "宜兴市"));
|
||||
areaList.add(new DemoCityData(7, 3, "新吴区"));
|
||||
areaList.add(new DemoCityData(8, 4, "鼓楼区"));
|
||||
areaList.add(new DemoCityData(9, 4, "丰县"));
|
||||
|
||||
selectParentData(cityList, areaList);
|
||||
|
||||
return areaList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟数据库的查询父数据操作
|
||||
*
|
||||
* @param parentList /
|
||||
* @param sonList /
|
||||
*/
|
||||
private void selectParentData(List<DemoCityData> parentList, List<DemoCityData> sonList) {
|
||||
Map<Integer, List<DemoCityData>> parentGroupByIdMap =
|
||||
parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
|
||||
|
||||
sonList.forEach(everySon -> {
|
||||
if (parentGroupByIdMap.containsKey(everySon.getPid())) {
|
||||
everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟的数据库省市县
|
||||
*/
|
||||
@Data
|
||||
private static class DemoCityData {
|
||||
/**
|
||||
* 数据库id字段
|
||||
*/
|
||||
private Integer id;
|
||||
/**
|
||||
* 数据库pid字段
|
||||
*/
|
||||
private Integer pid;
|
||||
/**
|
||||
* 数据库name字段
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* MyBatisPlus连带查询父数据
|
||||
*/
|
||||
private DemoCityData pData;
|
||||
|
||||
public DemoCityData(Integer id, Integer pid, String name) {
|
||||
this.id = id;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
|
||||
@Override
|
||||
public Cache getCache(String name) {
|
||||
// 重写 cacheName 支持多参数
|
||||
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
||||
name = array[0];
|
||||
|
||||
Cache cache = instanceMap.get(name);
|
||||
if (cache != null) {
|
||||
return cache;
|
||||
@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
configMap.put(name, config);
|
||||
}
|
||||
|
||||
// 重写 cacheName 支持多参数
|
||||
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
||||
name = array[0];
|
||||
if (array.length > 1) {
|
||||
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolation;
|
||||
@ -108,6 +110,26 @@ public class GlobalExceptionHandler {
|
||||
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求路径中缺少必需的路径变量
|
||||
*/
|
||||
@ExceptionHandler(MissingPathVariableException.class)
|
||||
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
|
||||
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数类型不匹配
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
|
||||
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件元数据
|
||||
*
|
||||
|
@ -1,45 +1,12 @@
|
||||
package com.ruoyi.sms.config;
|
||||
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.AliyunSmsTemplate;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import com.ruoyi.sms.core.TencentSmsTemplate;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 短信配置类
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
@Configuration
|
||||
//@Configuration // 暂时用不上 留着后续扩展使用
|
||||
public class SmsConfig {
|
||||
|
||||
// @Configuration
|
||||
// @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
||||
// @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
|
||||
// static class AliyunSmsConfig {
|
||||
//
|
||||
// @Bean
|
||||
// public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
|
||||
// return new AliyunSmsTemplate(smsProperties);
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
||||
@ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
|
||||
static class TencentSmsConfig {
|
||||
|
||||
@Bean
|
||||
public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
|
||||
return new TencentSmsTemplate(smsProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,47 +1,20 @@
|
||||
package com.ruoyi.sms.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* SMS短信 配置属性
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "sms")
|
||||
public class SmsProperties {
|
||||
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 配置节点
|
||||
* 阿里云 dysmsapi.aliyuncs.com
|
||||
* 腾讯云 sms.tencentcloudapi.com
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* key
|
||||
*/
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* 密匙
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/*
|
||||
* 短信签名
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* 短信应用ID (腾讯专属)
|
||||
*/
|
||||
private String sdkAppId;
|
||||
|
||||
}
|
||||
//package com.ruoyi.sms.config.properties;
|
||||
//
|
||||
//import lombok.Data;
|
||||
//import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
///**
|
||||
// * SMS短信 配置属性
|
||||
// *
|
||||
// * @author Lion Li
|
||||
// * @version 4.2.0
|
||||
// */
|
||||
//@Data
|
||||
//@Component
|
||||
//@ConfigurationProperties(prefix = "sms")
|
||||
//public class SmsProperties {
|
||||
//
|
||||
// private Boolean enabled;
|
||||
//
|
||||
//}
|
||||
|
@ -1,66 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import com.aliyun.dysmsapi20170525.Client;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||
import com.aliyun.teaopenapi.models.Config;
|
||||
import com.ruoyi.common.utils.JsonUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.sms.exception.SmsException;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Aliyun 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public class AliyunSmsTemplate implements SmsTemplate {
|
||||
|
||||
private SmsProperties properties;
|
||||
|
||||
private Client client;
|
||||
|
||||
@SneakyThrows(Exception.class)
|
||||
public AliyunSmsTemplate(SmsProperties smsProperties) {
|
||||
this.properties = smsProperties;
|
||||
Config config = new Config()
|
||||
// 您的AccessKey ID
|
||||
.setAccessKeyId(smsProperties.getAccessKeyId())
|
||||
// 您的AccessKey Secret
|
||||
.setAccessKeySecret(smsProperties.getAccessKeySecret())
|
||||
// 访问的域名
|
||||
.setEndpoint(smsProperties.getEndpoint());
|
||||
this.client = new Client(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
||||
if (StringUtils.isBlank(phones)) {
|
||||
throw new SmsException("手机号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(templateId)) {
|
||||
throw new SmsException("模板ID不能为空");
|
||||
}
|
||||
SendSmsRequest req = new SendSmsRequest()
|
||||
.setPhoneNumbers(phones)
|
||||
.setSignName(properties.getSignName())
|
||||
.setTemplateCode(templateId)
|
||||
.setTemplateParam(JsonUtils.toJsonString(param));
|
||||
try {
|
||||
SendSmsResponse resp = client.sendSms(req);
|
||||
return SmsResult.builder()
|
||||
.isSuccess("OK".equals(resp.getBody().getCode()))
|
||||
.message(resp.getBody().getMessage())
|
||||
.response(JsonUtils.toJsonString(resp))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new SmsException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public interface SmsTemplate {
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*
|
||||
* @param phones 电话号(多个逗号分割)
|
||||
* @param templateId 模板id
|
||||
* @param param 模板对应参数
|
||||
* 阿里 需使用 模板变量名称对应内容 例如: code=1234
|
||||
* 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
|
||||
*/
|
||||
SmsResult send(String phones, String templateId, Map<String, String> param);
|
||||
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package com.ruoyi.sms.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.ruoyi.common.utils.JsonUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.sms.exception.SmsException;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.sms.v20190711.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
|
||||
import com.tencentcloudapi.sms.v20190711.models.SendStatus;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Tencent 短信模板
|
||||
*
|
||||
* @author Lion Li
|
||||
* @version 4.2.0
|
||||
*/
|
||||
public class TencentSmsTemplate implements SmsTemplate {
|
||||
|
||||
private SmsProperties properties;
|
||||
|
||||
private SmsClient client;
|
||||
|
||||
@SneakyThrows(Exception.class)
|
||||
public TencentSmsTemplate(SmsProperties smsProperties) {
|
||||
this.properties = smsProperties;
|
||||
Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint(smsProperties.getEndpoint());
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
this.client = new SmsClient(credential, "", clientProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
||||
if (StringUtils.isBlank(phones)) {
|
||||
throw new SmsException("手机号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(templateId)) {
|
||||
throw new SmsException("模板ID不能为空");
|
||||
}
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
|
||||
req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
|
||||
if (CollUtil.isNotEmpty(param)) {
|
||||
req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
|
||||
}
|
||||
req.setTemplateID(templateId);
|
||||
req.setSign(properties.getSignName());
|
||||
req.setSmsSdkAppid(properties.getSdkAppId());
|
||||
try {
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
SmsResult.SmsResultBuilder builder = SmsResult.builder()
|
||||
.isSuccess(true)
|
||||
.message("send success")
|
||||
.response(JsonUtils.toJsonString(resp));
|
||||
for (SendStatus sendStatus : resp.getSendStatusSet()) {
|
||||
if (!"Ok".equals(sendStatus.getCode())) {
|
||||
builder.isSuccess(false).message(sendStatus.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
} catch (Exception e) {
|
||||
throw new SmsException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package com.ruoyi.sms.entity;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 上传返回体
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class SmsResult {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean isSuccess;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 实际响应体
|
||||
* <p>
|
||||
* 可自行转换为 SDK 对应的 SendSmsResponse
|
||||
*/
|
||||
private String response;
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package com.ruoyi.sms.exception;
|
||||
|
||||
/**
|
||||
* Sms异常类
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class SmsException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public SmsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
@ -2,12 +2,12 @@ package com.ruoyi.system.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.system.domain.SysOss;
|
||||
import com.ruoyi.system.domain.bo.SysOssBo;
|
||||
import com.ruoyi.system.domain.vo.SysOssVo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -27,6 +27,8 @@ public interface ISysOssService {
|
||||
|
||||
SysOssVo upload(MultipartFile file);
|
||||
|
||||
SysOssVo upload(File file);
|
||||
|
||||
void download(Long ossId, HttpServletResponse response) throws IOException;
|
||||
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
@ -8,9 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.domain.event.LogininforEvent;
|
||||
import com.ruoyi.common.core.domain.dto.RoleDTO;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.event.LogininforEvent;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.domain.model.XcxLoginUser;
|
||||
import com.ruoyi.common.enums.DeviceType;
|
||||
@ -71,9 +71,10 @@ public class SysLoginService {
|
||||
if (captchaEnabled) {
|
||||
validateCaptcha(username, code, uuid);
|
||||
}
|
||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
||||
SysUser user = loadUserByUsername(username);
|
||||
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
|
||||
@ -88,7 +89,7 @@ public class SysLoginService {
|
||||
SysUser user = loadUserByPhonenumber(phonenumber);
|
||||
|
||||
checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
||||
@ -99,11 +100,11 @@ public class SysLoginService {
|
||||
}
|
||||
|
||||
public String emailLogin(String email, String emailCode) {
|
||||
// 通过手机号查找用户
|
||||
// 通过手邮箱查找用户
|
||||
SysUser user = loadUserByEmail(email);
|
||||
|
||||
checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
LoginUser loginUser = buildLoginUser(user);
|
||||
// 生成token
|
||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
||||
@ -118,9 +119,11 @@ public class SysLoginService {
|
||||
// todo 以下自行实现
|
||||
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
||||
String openid = "";
|
||||
|
||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
||||
SysUser user = loadUserByOpenid(openid);
|
||||
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||
XcxLoginUser loginUser = new XcxLoginUser();
|
||||
loginUser.setUserId(user.getUserId());
|
||||
loginUser.setUsername(user.getUserName());
|
||||
@ -301,25 +304,24 @@ public class SysLoginService {
|
||||
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
|
||||
String loginFail = Constants.LOGIN_FAIL;
|
||||
|
||||
// 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
|
||||
Integer errorNumber = RedisUtils.getCacheObject(errorKey);
|
||||
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||
// 锁定时间内登录 则踢出
|
||||
if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
|
||||
if (errorNumber >= maxRetryCount) {
|
||||
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||
}
|
||||
|
||||
if (supplier.get()) {
|
||||
// 是否第一次
|
||||
errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
|
||||
// 错误次数递增
|
||||
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);
|
||||
}
|
||||
|
@ -116,7 +116,6 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
* @param dictType 字典类型
|
||||
* @return 字典类型
|
||||
*/
|
||||
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
|
||||
@Override
|
||||
public SysDictType selectDictTypeByType(String dictType) {
|
||||
return baseMapper.selectById(new LambdaQueryWrapper<SysDictType>().eq(SysDictType::getDictType, dictType));
|
||||
@ -148,7 +147,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
List<SysDictData> dictDataList = dictDataMapper.selectList(
|
||||
new LambdaQueryWrapper<SysDictData>().eq(SysDictData::getStatus, UserConstants.DICT_NORMAL));
|
||||
Map<String, List<SysDictData>> dictDataMap = StreamUtils.groupByKey(dictDataList, SysDictData::getDictType);
|
||||
dictDataMap.forEach((k,v) -> {
|
||||
dictDataMap.forEach((k, v) -> {
|
||||
List<SysDictData> dictList = StreamUtils.sorted(v, Comparator.comparing(SysDictData::getDictSort));
|
||||
CacheUtils.put(CacheNames.SYS_DICT, k, dictList);
|
||||
});
|
||||
@ -182,6 +181,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
public List<SysDictData> insertDictType(SysDictType dict) {
|
||||
int row = baseMapper.insert(dict);
|
||||
if (row > 0) {
|
||||
// 新增 type 下无 data 数据 返回空防止缓存穿透
|
||||
return new ArrayList<>();
|
||||
}
|
||||
throw new ServiceException("操作失败");
|
||||
@ -279,4 +279,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getAllDictByDictType(String dictType) {
|
||||
List<SysDictData> list = selectDictDataByType(dictType);
|
||||
return StreamUtils.toMap(list, SysDictData::getDictValue, SysDictData::getDictLabel);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
@ -11,7 +12,6 @@ import com.ruoyi.common.core.domain.PageQuery;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.service.OssService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.BeanCopyUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
@ -31,9 +31,13 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -108,7 +112,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
}
|
||||
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
|
||||
OssClient storage = OssFactory.instance();
|
||||
OssClient storage = OssFactory.instance(sysOss.getService());
|
||||
try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
|
||||
int available = inputStream.available();
|
||||
IoUtil.copy(inputStream, response.getOutputStream(), available);
|
||||
@ -130,15 +134,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
// 保存文件信息
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysOssVo upload(File file) {
|
||||
String originalfileName = file.getName();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance();
|
||||
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
|
||||
// 保存文件信息
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
|
||||
}
|
||||
|
||||
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
|
||||
SysOss oss = new SysOss();
|
||||
oss.setUrl(uploadResult.getUrl());
|
||||
oss.setFileSuffix(suffix);
|
||||
oss.setFileName(uploadResult.getFilename());
|
||||
oss.setOriginalName(originalfileName);
|
||||
oss.setService(storage.getConfigKey());
|
||||
oss.setService(configKey);
|
||||
baseMapper.insert(oss);
|
||||
SysOssVo sysOssVo = new SysOssVo();
|
||||
BeanCopyUtils.copy(oss, sysOssVo);
|
||||
SysOssVo sysOssVo = BeanUtil.toBean(oss, SysOssVo.class);
|
||||
return this.matchingUrl(sysOssVo);
|
||||
}
|
||||
|
||||
|
@ -186,6 +186,20 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
if (ObjectUtil.isNotNull(role.getRoleId()) && role.isAdmin()) {
|
||||
throw new ServiceException("不允许操作超级管理员角色");
|
||||
}
|
||||
// 新增不允许使用 管理员标识符
|
||||
if (ObjectUtil.isNull(role.getRoleId())
|
||||
&& StringUtils.equals(role.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
|
||||
throw new ServiceException("不允许使用系统内置管理员角色标识符!");
|
||||
}
|
||||
// 修改不允许修改 管理员标识符
|
||||
if (ObjectUtil.isNotNull(role.getRoleId())) {
|
||||
SysRole sysRole = baseMapper.selectById(role.getRoleId());
|
||||
// 如果标识符不相等 判断为修改了管理员标识符
|
||||
if (!StringUtils.equals(sysRole.getRoleKey(), role.getRoleKey())
|
||||
&& StringUtils.equals(sysRole.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
|
||||
throw new ServiceException("不允许修改系统内置管理员角色标识符!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,9 +356,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteRoleByIds(Long[] roleIds) {
|
||||
for (Long roleId : roleIds) {
|
||||
checkRoleAllowed(new SysRole(roleId));
|
||||
checkRoleDataScope(roleId);
|
||||
SysRole role = selectRoleById(roleId);
|
||||
checkRoleAllowed(role);
|
||||
checkRoleDataScope(roleId);
|
||||
if (countUserRoleByRoleId(roleId) > 0) {
|
||||
throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
|
||||
}
|
||||
@ -420,6 +434,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
|
||||
@Override
|
||||
public void cleanOnlineUserByRole(Long roleId) {
|
||||
// 如果角色未绑定用户 直接返回
|
||||
Long num = userRoleMapper.selectCount(new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getRoleId, roleId));
|
||||
if (num == 0) {
|
||||
return;
|
||||
}
|
||||
List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
|
||||
if (CollUtil.isEmpty(keys)) {
|
||||
return;
|
||||
@ -428,11 +447,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
|
||||
keys.parallelStream().forEach(key -> {
|
||||
String token = StringUtils.substringAfterLast(key, ":");
|
||||
// 如果已经过期则跳过
|
||||
if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
|
||||
if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
|
||||
return;
|
||||
}
|
||||
LoginUser loginUser = LoginHelper.getLoginUser(token);
|
||||
if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
|
||||
if (ObjectUtil.isNotNull(loginUser) && loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
|
||||
try {
|
||||
StpUtil.logoutByTokenValue(token);
|
||||
} catch (NotLoginException ignored) {
|
||||
|
@ -16,12 +16,13 @@ import com.ruoyi.common.utils.reflect.ReflectUtils;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import com.ruoyi.framework.config.properties.CaptchaProperties;
|
||||
import com.ruoyi.framework.config.properties.MailProperties;
|
||||
import com.ruoyi.sms.config.properties.SmsProperties;
|
||||
import com.ruoyi.sms.core.SmsTemplate;
|
||||
import com.ruoyi.sms.entity.SmsResult;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
import org.dromara.sms4j.provider.enumerate.SupplierType;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -47,7 +49,6 @@ import java.util.Map;
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final SmsProperties smsProperties;
|
||||
private final ISysConfigService configService;
|
||||
private final MailProperties mailProperties;
|
||||
|
||||
@ -58,21 +59,18 @@ public class CaptchaController {
|
||||
*/
|
||||
@GetMapping("/captchaSms")
|
||||
public R<Void> smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
||||
if (!smsProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启短信功能!");
|
||||
}
|
||||
String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||
String code = RandomUtil.randomNumbers(4);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "";
|
||||
Map<String, String> map = new HashMap<>(1);
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
|
||||
SmsResult result = smsTemplate.send(phonenumber, templateId, map);
|
||||
if (!result.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", result);
|
||||
return R.fail(result.getMessage());
|
||||
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
|
||||
if (!"OK".equals(smsResponse.getCode())) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(smsResponse.getMessage());
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ public class SysUserOnlineController extends BaseController {
|
||||
for (String key : keys) {
|
||||
String token = StringUtils.substringAfterLast(key, ":");
|
||||
// 如果已经过期则跳过
|
||||
if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
|
||||
if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
|
||||
continue;
|
||||
}
|
||||
userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));
|
||||
|
@ -80,6 +80,7 @@ public class SysRoleController extends BaseController {
|
||||
@Log(title = "角色管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody SysRole role) {
|
||||
roleService.checkRoleAllowed(role);
|
||||
if (!roleService.checkRoleNameUnique(role)) {
|
||||
return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
|
||||
} else if (!roleService.checkRoleKeyUnique(role)) {
|
||||
|
@ -158,14 +158,29 @@ mail:
|
||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||
connectionTimeout: 0
|
||||
|
||||
--- # sms 短信
|
||||
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||
# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
|
||||
sms:
|
||||
enabled: false
|
||||
# 阿里云 dysmsapi.aliyuncs.com
|
||||
# 腾讯云 sms.tencentcloudapi.com
|
||||
endpoint: "dysmsapi.aliyuncs.com"
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user