fix 修复 一些发现的小bug

- 短信验证码限流 key 改为 #phoneNumber,验证码改为发送成功后再写 Redis,失败消息避免 NPE
- 邮箱验证码改为邮件发送成功后再写 Redis
- ValidatorUtils 增加 NPE 校验
- 部门导入路径不存在时明确报错,不再静默转成 null
- 重复提交切面完整扫描数组、集合、Map 中的过滤对象
- Excel 级联下拉补齐一级选项的 Excel 名称规则校验和重复校验
This commit is contained in:
疯狂的狮子Li
2026-06-08 16:37:18 +08:00
parent 14f96acd60
commit 3a6d62d695
6 changed files with 71 additions and 12 deletions
@@ -58,7 +58,7 @@ public class CaptchaController {
* @param phoneNumber 用户手机号
* @return 操作结果
*/
@RateLimiter(key = "#phonenumber", time = 60, count = 1)
@RateLimiter(key = "#phoneNumber", time = 60, count = 1)
@GetMapping("/resource/sms/code")
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phoneNumber) {
if (!RegexValidator.isMobile(phoneNumber)) {
@@ -66,7 +66,6 @@ public class CaptchaController {
}
String key = GlobalConstants.CAPTCHA_CODE_KEY + phoneNumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
@@ -75,8 +74,10 @@ public class CaptchaController {
SmsResponse smsResponse = smsBlend.sendMessage(phoneNumber, templateId, map);
if (!smsResponse.isSuccess()) {
log.error("验证码短信发送异常 => {}", smsResponse);
return R.fail(smsResponse.getData().toString());
Object data = smsResponse.getData();
return R.fail(data == null ? "验证码短信发送失败" : data.toString());
}
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
return R.ok();
}
@@ -107,13 +108,13 @@ public class CaptchaController {
public void emailCodeImpl(String email) {
String key = GlobalConstants.CAPTCHA_CODE_KEY + email;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
try {
MailBuilder.of()
.to(email)
.subject("登录验证码")
.text("您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。")
.send();
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
throw new ServiceException(e.getMessage());
@@ -26,6 +26,9 @@ public class ValidatorUtils {
* @throws ConstraintViolationException 如果校验不通过,则抛出参数校验异常
*/
public static <T> void validate(T object, Class<?>... groups) {
if (object == null) {
throw new RuntimeException("请求参数不能为空");
}
Set<ConstraintViolation<T>> validate = VALID.validate(object, groups);
if (!validate.isEmpty()) {
throw new ConstraintViolationException("参数校验异常", validate);
@@ -51,6 +51,9 @@ public class DropDownOptions {
* 分隔符
*/
private static final String DELIMITER = "_";
private static final String OPTION_PART_REGEX = "^[A-Za-z0-9\\u4e00-\\u9fa5]+$";
private static final String EXCEL_NAME_REGEX = "^[A-Za-z_\\u4e00-\\u9fa5][A-Za-z0-9_\\u4e00-\\u9fa5]*$";
private static final String CELL_REFERENCE_REGEX = "^[A-Za-z]{1,3}[1-9][0-9]*$";
/**
* 创建只有一级的下拉选
@@ -69,10 +72,9 @@ public class DropDownOptions {
*/
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(Convert.toStr(vars[i]));
if (!var.matches(regex)) {
if (!var.matches(OPTION_PART_REGEX)) {
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
}
stringBuffer.append(var);
@@ -81,10 +83,29 @@ public class DropDownOptions {
stringBuffer.append(DELIMITER);
}
}
if (stringBuffer.toString().matches("^\\d_*$")) {
String optionValue = stringBuffer.toString();
validateOptionValue(optionValue);
return optionValue;
}
/**
* 校验级联下拉选项值是否可作为 Excel 名称管理器名称。
*
* @param optionValue 选项值
*/
public static void validateOptionValue(String optionValue) {
if (StrUtil.isBlank(optionValue)) {
throw new ServiceException("选项数据不能为空");
}
if (optionValue.matches("^[0-9].*")) {
throw new ServiceException("禁止以数字开头");
}
return stringBuffer.toString();
if (!optionValue.matches(EXCEL_NAME_REGEX)) {
throw new ServiceException("选项数据不符合Excel名称规则,仅允许中英文、数字或下划线,且不能以数字开头");
}
if (optionValue.matches(CELL_REFERENCE_REGEX)) {
throw new ServiceException("选项数据不能为Excel单元格引用");
}
}
/**
@@ -202,6 +202,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
if (CollUtil.isEmpty(firstOptions)) {
return;
}
validateLinkedOptionNames(firstOptions);
Map<String, List<String>> secoundOptionsMap = new HashMap<>();
options.getNextOptions().forEach((k, v) -> secoundOptionsMap.put(k, new ArrayList<>(v)));
@@ -313,6 +314,19 @@ public class ExcelDownHandler implements SheetWriteHandler {
currentLinkedOptionsSheetIndex++;
}
/**
* 校验级联下拉一级选项可作为 Excel 名称,且不能重复。
*/
private void validateLinkedOptionNames(List<String> firstOptions) {
Set<String> names = new HashSet<>();
for (String firstOption : firstOptions) {
DropDownOptions.validateOptionValue(firstOption);
if (!names.add(firstOption)) {
throw new ServiceException("级联下拉一级选项重复:" + firstOption);
}
}
}
/**
* <h2>额外表格形式的普通下拉框</h2>
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
@@ -24,6 +24,7 @@ import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Array;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
@@ -150,16 +151,30 @@ public class RepeatSubmitAspect {
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
if (MultipartFile.class.isAssignableFrom(clazz.getComponentType())) {
return true;
}
int length = Array.getLength(o);
for (int i = 0; i < length; i++) {
Object value = Array.get(o, i);
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
return false;
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
return value instanceof MultipartFile;
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
return value instanceof MultipartFile;
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
@@ -10,6 +10,7 @@ import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.ReadCellData;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.TreeBuildUtils;
@@ -93,7 +94,11 @@ public class DeptExcelConverter implements Converter<Long> {
if (StringUtils.isBlank(deptName)) {
return null;
}
return getDeptMaps().nameToId().get(deptName);
Long deptId = getDeptMaps().nameToId().get(deptName);
if (deptId == null) {
throw new ServiceException("部门不存在:" + deptName);
}
return deptId;
}
/**