Compare commits

..

17 Commits

Author SHA1 Message Date
232624aace release: v2.7.3 2024-11-15 21:58:40 +08:00
2e9079a909 refactor(core): 拆分字符串常量和字符常量 2024-11-15 21:54:06 +08:00
c7bee0033e feat(data/mp): 新增枚举校验器 2024-11-15 21:53:52 +08:00
efb84c936f fix(data/mp): 修复普通枚举类型处理错误 2024-11-15 20:27:08 +08:00
4b77d5cb3f chore(extension/crud): 查询详情命名调整,GET -> DETAIL,增加详情权限校验 2024-11-14 20:32:40 +08:00
e3433bed01 chore: 升级依赖 CosID 2.9.8 => 2.9.9 2024-11-14 20:27:02 +08:00
56edceec7e feat(cache/redisson): RedisUtils 新增 ZSet 相关方法 2024-11-14 20:24:28 +08:00
7cd3935d71 release: v2.7.2 2024-11-12 21:47:17 +08:00
1ce5eb3b73 refactor(data): 重构 MetaUtils 获取表信息方法 2024-11-12 21:40:07 +08:00
a8c6ea3079 feat(extension/crud): DictField extraKey => extraKeys 2024-11-12 21:39:06 +08:00
9b7ea33c0b feat(extension/crud): 查询字典列表新增支持 extraKey 额外信息字段 2024-11-08 20:57:04 +08:00
e9b9d8b82e refactor(core): 重构 IP 工具类获取归属地的返回格式(更方便数据处理) 2024-11-08 20:49:12 +08:00
04498ffe56 feat(cache/redisson): RedisUtils 新增上锁、释放锁方法 2024-11-07 20:29:47 +08:00
bd60411f3e chore(data/mp): 移除冗余的数据库类型判空处理 2024-11-05 22:22:14 +08:00
673e586aaf refactor(data): Query 范围查询支持数组数据 2024-10-31 20:40:18 +08:00
f4b23102a9 chore(extension/crud): 移除 TreeUtils 2024-10-16 22:37:32 +08:00
5891c4aa61 feat(extension/crud): 支持树结构全局配置 2024-10-13 21:23:31 +08:00
38 changed files with 1947 additions and 598 deletions

View File

@@ -1,3 +1,39 @@
## [v2.7.3](https://github.com/continew-org/continew-starter/compare/v2.7.2...v2.7.3) (2024-11-15)
### ✨ 新特性
- 【cache/redisson】RedisUtils 新增 ZSet 相关方法 ([56edcee](https://github.com/continew-org/continew-starter/commit/56edceec7e7c61bbd06a1995c2351441302ac969))
- 【core】新增枚举校验器 EnumValue ([c7bee00](https://github.com/continew-org/continew-starter/commit/c7bee0033ef794784b9c9fd39f61a245abff0c62))
### 💎 功能优化
- 【extension/crud】查询详情命名调整GET -> DETAIL增加详情权限校验 ([4b77d5c](https://github.com/continew-org/continew-starter/commit/4b77d5cb3ff93e7d8d207196948352294c6cdcc6))
- 【core】拆分字符串常量和字符常量 ([2e9079a](https://github.com/continew-org/continew-starter/commit/2e9079a909db8df57ed7de49c95d9daeb9616f4a))
### 🐛 问题修复
- 【data/mp】修复普通枚举类型处理错误 ([efb84c9](https://github.com/continew-org/continew-starter/commit/efb84c936f1012c3ac4b6264599f6fb1f5ae5f97))
### 📦 依赖升级
- CosID 2.9.8 => 2.9.9 ([e3433be](https://github.com/continew-org/continew-starter/commit/e3433bed01e9bccc1179c04acc82df843434d9af))
## [v2.7.2](https://github.com/continew-org/continew-starter/compare/v2.7.1...v2.7.2) (2024-11-12)
### ✨ 新特性
- 【extension/crud】支持树结构全局配置 ([5891c4a](https://github.com/continew-org/continew-starter/commit/5891c4aa61b14ba11a387a478fb3616dfc52217c))
- 【extension/crud】查询字典列表新增支持 extraKeys 额外信息字段 ([9b7ea33](https://github.com/continew-org/continew-starter/commit/9b7ea33c0b6714e2ea631aa26f0650e78857079a)) ([a8c6ea3](https://github.com/continew-org/continew-starter/commit/a8c6ea30797811d885f294f28eb95afb935ad7b4))
- 【cache/redisson】RedisUtils 新增上锁、释放锁方法 ([04498ff](https://github.com/continew-org/continew-starter/commit/04498ffe56b062bce1200292b23d2c31341771e6))
### 💎 功能优化
- 【extension/crud】移除 TreeUtils ([f4b2310](https://github.com/continew-org/continew-starter/commit/f4b23102a9a31b2120f40a8288bc0aedc36e11b4))
- 【data/mp】移除冗余的数据库类型判空处理 ([bd60411](https://github.com/continew-org/continew-starter/commit/bd60411f3e4fa87c26e492df96fbfb088ea3ce85))
- 【core】重构 IP 工具类获取归属地的返回格式(更方便数据处理) ([e9b9d8b](https://github.com/continew-org/continew-starter/commit/e9b9d8b82e7e28be82c9ed518582d88f507cfac2))
- 【data】Query 范围查询支持数组数据 ([673e586](https://github.com/continew-org/continew-starter/commit/673e586aafc8578f0c7ab063ca9df9b1265f88d5))
- 【data】重构 MetaUtils 获取表信息方法 ([1ce5eb3](https://github.com/continew-org/continew-starter/commit/1ce5eb3b734b13ccd47e3848117daf3c2d7d0afa))
## [v2.7.0](https://github.com/continew-org/continew-starter/compare/v2.6.0...v2.7.0) (2024-09-28) ## [v2.7.0](https://github.com/continew-org/continew-starter/compare/v2.6.0...v2.7.0) (2024-09-28)
### ✨ 新特性 ### ✨ 新特性

View File

@@ -16,8 +16,8 @@
package top.continew.starter.apidoc.handler; package top.continew.starter.apidoc.handler;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.AnnotatedType;
@@ -52,7 +52,7 @@ public class BaseEnumParameterHandler implements ParameterCustomizer, PropertyCu
return parameterModel; return parameterModel;
} }
String description = parameterModel.getDescription(); String description = parameterModel.getDescription();
if (StrUtil.contains(description, "color:red")) { if (CharSequenceUtil.contains(description, "color:red")) {
return parameterModel; return parameterModel;
} }
// 自定义枚举描述并封装参数配置 // 自定义枚举描述并封装参数配置

View File

@@ -24,6 +24,8 @@ import top.continew.starter.core.constant.StringConstants;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/** /**
* Redis 工具类 * Redis 工具类
@@ -192,6 +194,167 @@ public class RedisUtils {
return CLIENT.getKeys().getKeysStreamByPattern(pattern).toList(); return CLIENT.getKeys().getKeysStreamByPattern(pattern).toList();
} }
/**
* 添加元素到 ZSet 中
*
* @param key 键
* @param value 值
* @param score 分数
* @return true添加成功false添加失败
* @since 2.7.3
*/
public static <T> boolean zAdd(String key, T value, double score) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.add(score, value);
}
/**
* 查询 ZSet 中指定元素的分数
*
* @param key 键
* @param value 值
* @return 分数null 表示元素不存在)
* @since 2.7.3
*/
public static <T> Double zScore(String key, T value) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.getScore(value);
}
/**
* 查询 ZSet 中指定元素的排名
*
* @param key 键
* @param value 值
* @return 排名(从 0 开始null 表示元素不存在)
* @since 2.7.3
*/
public static <T> Integer zRank(String key, T value) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.rank(value);
}
/**
* 查询 ZSet 中的元素个数
*
* @param key 键
* @return 元素个数
* @since 2.7.3
*/
public static <T> int zSize(String key) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.size();
}
/**
* 从 ZSet 中删除指定元素
*
* @param key 键
* @param value 值
* @return true删除成功false删除失败
* @since 2.7.3
*/
public static <T> boolean zRemove(String key, T value) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.remove(value);
}
/**
* 删除 ZSet 中指定分数范围内的元素
*
* @param key 键
* @param min 最小分数(包含)
* @param max 最大分数(包含)
* @return 删除的元素个数
* @since 2.7.3
*/
public static <T> int zRemoveRangeByScore(String key, double min, double max) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.removeRangeByScore(min, true, max, true);
}
/**
* 删除 ZSet 中指定排名范围内的元素
*
* <p>
* 索引从 0 开始。<code>-1<code> 表示最高分,<code>-2<code> 表示第二高分。
* </p>
*
* @param key 键
* @param startIndex 起始索引
* @param endIndex 结束索引
* @return 删除的元素个数
* @since 2.7.3
*/
public static <T> int zRemoveRangeByRank(String key, int startIndex, int endIndex) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.removeRangeByRank(startIndex, endIndex);
}
/**
* 根据分数范围查询 ZSet 中的元素列表
*
* @param key 键
* @param min 最小分数(包含)
* @param max 最大分数(包含)
* @return 元素列表
* @since 2.7.3
*/
public static <T> Collection<T> zRangeByScore(String key, double min, double max) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.valueRange(min, true, max, true);
}
/**
* 根据分数范围查询 ZSet 中的元素列表
*
* @param key 键
* @param min 最小分数(包含)
* @param max 最大分数(包含)
* @param offset 偏移量
* @param count 数量
* @return 元素列表
* @since 2.7.3
*/
public static <T> Collection<T> zRangeByScore(String key, double min, double max, int offset, int count) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.valueRange(min, true, max, true, offset, count);
}
/**
* 根据分数范围查询 ZSet 中的元素个数
*
* @param key 键
* @param min 最小分数(包含)
* @param max 最大分数(包含)
* @return 元素个数
* @since 2.7.3
*/
public static <T> int zCountRangeByScore(String key, double min, double max) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
return zSet.count(min, true, max, true);
}
/**
* 计算 ZSet 中多个元素的分数之和
*
* @param key 键
* @param values 值列表
* @return 分数之和
* @since 2.7.3
*/
public static <T> double zSum(String key, Collection<T> values) {
RScoredSortedSet<T> zSet = CLIENT.getScoredSortedSet(key);
double sum = 0;
for (T value : values) {
Double score = zSet.getScore(value);
if (score != null) {
sum += score;
}
}
return sum;
}
/** /**
* 限流 * 限流
* *
@@ -207,6 +370,80 @@ public class RedisUtils {
return rateLimiter.tryAcquire(1); return rateLimiter.tryAcquire(1);
} }
/**
* 尝试获取锁
*
* @param key 键
* @param expireTime 锁过期时间(单位:毫秒)
* @param timeout 获取锁超时时间(单位:毫秒)
* @return true成功false失败
* @since 2.7.2
*/
public static boolean tryLock(String key, long expireTime, long timeout) {
return tryLock(key, expireTime, timeout, TimeUnit.MILLISECONDS);
}
/**
* 释放锁
*
* @param key 键
* @return true释放成功false释放失败
* @since 2.7.2
*/
public static boolean unlock(String key) {
RLock lock = getLock(key);
return unlock(lock);
}
/**
* 尝试获取锁
*
* @param key 键
* @param expireTime 锁过期时间
* @param timeout 获取锁超时时间
* @param unit 时间单位
* @return true成功false失败
* @since 2.7.2
*/
public static boolean tryLock(String key, long expireTime, long timeout, TimeUnit unit) {
RLock lock = getLock(key);
try {
return lock.tryLock(timeout, expireTime, unit);
} catch (InterruptedException e) {
return false;
}
}
/**
* 释放锁
*
* @param lock 锁实例
* @return true释放成功false释放失败
* @since 2.7.2
*/
public static boolean unlock(RLock lock) {
if (lock.isHeldByCurrentThread()) {
try {
lock.unlockAsync().get();
return true;
} catch (ExecutionException | InterruptedException e) {
return false;
}
}
return false;
}
/**
* 获取锁实例
*
* @param key 键
* @return 锁实例
* @since 2.7.2
*/
public static RLock getLock(String key) {
return CLIENT.getLock(key);
}
/** /**
* 格式化键,将各子键用 : 拼接起来 * 格式化键,将各子键用 : 拼接起来
* *

View File

@@ -0,0 +1,166 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.constant;
/**
* 字符相关常量
*
* @author looly<a href="https://gitee.com/dromara/hutool">Hutool</a>
* @author Charles7c
* @see cn.hutool.core.text.CharPool
* @since 2.7.3
*/
public class CharConstants {
/**
* 空格符 {@code ' '}
*/
public static final char SPACE = ' ';
/**
* 制表符 {@code '\t'}
*/
public static final char TAB = ' ';
/**
* 点 {@code '.'}
*/
public static final char DOT = '.';
/**
* 逗号 {@code ','}
*/
public static final char COMMA = ',';
/**
* 中文逗号 {@code ''}
*/
public static final char CHINESE_COMMA = '';
/**
* 冒号 {@code ':'}
*/
public static final char COLON = ':';
/**
* 分号 {@code ';'}
*/
public static final char SEMICOLON = ';';
/**
* 问号 {@code '?'}
*/
public static final char QUESTION_MARK = '?';
/**
* 下划线 {@code '_'}
*/
public static final char UNDERLINE = '_';
/**
* 减号(连接符) {@code '-'}
*/
public static final char DASHED = '-';
/**
* 星号 {@code '*'}
*/
public static final char ASTERISK = '*';
/**
* 斜杠 {@code '/'}
*/
public static final char SLASH = '/';
/**
* 反斜杠 {@code '\\'}
*/
public static final char BACKSLASH = '\\';
/**
* 管道符 {@code '|'}
*/
public static final char PIPE = '|';
/**
* 艾特 {@code '@'}
*/
public static final char AT = '@';
/**
* 与符号 {@code '&'}
*/
public static final char AMP = '&';
/**
* 花括号(左) <code>'{'</code>
*/
public static final char DELIM_START = '{';
/**
* 花括号(右) <code>'}'</code>
*/
public static final char DELIM_END = '}';
/**
* 中括号(左) {@code '['}
*/
public static final char BRACKET_START = '[';
/**
* 中括号(右) {@code ']'}
*/
public static final char BRACKET_END = ']';
/**
* 圆括号(左) {@code '('}
*/
public static final char ROUND_BRACKET_START = '(';
/**
* 圆括号(右) {@code ')'}
*/
public static final char ROUND_BRACKET_END = ')';
/**
* 双引号 {@code '"'}
*/
public static final char DOUBLE_QUOTES = '"';
/**
* 单引号 {@code '\''}
*/
public static final char SINGLE_QUOTE = '\'';
/**
* 等号 {@code '='}
*/
public static final char EQUALS = '=';
/**
* 回车符 {@code '\r'}
*/
public static final char CR = '\r';
/**
* 换行符 {@code '\n'}
*/
public static final char LF = '\n';
private CharConstants() {
}
}

View File

@@ -119,6 +119,11 @@ public class PropertiesConstants {
*/ */
public static final String MESSAGING_WEBSOCKET = MESSAGING + StringConstants.DOT + "websocket"; public static final String MESSAGING_WEBSOCKET = MESSAGING + StringConstants.DOT + "websocket";
/**
* CRUD 配置
*/
public static final String CRUD = CONTINEW_STARTER + StringConstants.DOT + "crud";
/** /**
* 数据权限配置 * 数据权限配置
*/ */

View File

@@ -16,248 +16,133 @@
package top.continew.starter.core.constant; package top.continew.starter.core.constant;
import cn.hutool.core.text.CharPool;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.XmlUtil;
/** /**
* 字符串相关常量 * 字符串相关常量
* *
* @author looly * @author looly<a href="https://gitee.com/dromara/hutool">Hutool</a>
* @author Charles7c * @author Charles7c
* @see cn.hutool.core.text.StrPool
* @since 1.0.0 * @since 1.0.0
*/ */
public class StringConstants { public class StringConstants {
/** /**
* 字符常量:空格符 {@code ' '} * 字符 {@code ""}
*/
public static final char C_SPACE = CharPool.SPACE;
/**
* 字符常量:制表符 {@code '\t'}
*/
public static final char C_TAB = CharPool.TAB;
/**
* 字符常量:点 {@code '.'}
*/
public static final char C_DOT = CharPool.DOT;
/**
* 字符常量:斜杠 {@code '/'}
*/
public static final char C_SLASH = CharPool.SLASH;
/**
* 字符常量:反斜杠 {@code '\\'}
*/
public static final char C_BACKSLASH = CharPool.BACKSLASH;
/**
* 字符常量:回车符 {@code '\r'}
*/
public static final char C_CR = CharPool.CR;
/**
* 字符常量:换行符 {@code '\n'}
*/
public static final char C_LF = CharPool.LF;
/**
* 字符常量:下划线 {@code '_'}
*/
public static final char C_UNDERLINE = CharPool.UNDERLINE;
/**
* 字符常量:逗号 {@code ','}
*/
public static final char C_COMMA = CharPool.COMMA;
/**
* 字符常量:花括号(左) <code>'{'</code>
*/
public static final char C_DELIM_START = CharPool.DELIM_START;
/**
* 字符常量:花括号(右) <code>'}'</code>
*/
public static final char C_DELIM_END = CharPool.DELIM_END;
/**
* 字符常量:中括号(左) {@code '['}
*/
public static final char C_BRACKET_START = CharPool.BRACKET_START;
/**
* 字符常量:中括号(右) {@code ']'}
*/
public static final char C_BRACKET_END = CharPool.BRACKET_END;
/**
* 字符常量:冒号 {@code ':'}
*/
public static final char C_COLON = CharPool.COLON;
/**
* 字符常量:艾特 {@code '@'}
*/
public static final char C_AT = CharPool.AT;
/**
* 字符常量:星号 {@code '*'}
*/
public static final char C_ASTERISK = '*';
/**
* 字符串常量:制表符 {@code "\t"}
*/
public static final String TAB = StrPool.TAB;
/**
* 字符串常量:点 {@code "."}
*/
public static final String DOT = StrPool.DOT;
/**
* 字符串常量:双点 {@code ".."} <br> 用途:作为指向上级文件夹的路径,如:{@code "../path"}
*/
public static final String DOUBLE_DOT = StrPool.DOUBLE_DOT;
/**
* 字符串常量:斜杠 {@code "/"}
*/
public static final String SLASH = StrPool.SLASH;
/**
* 字符串常量:反斜杠 {@code "\\"}
*/
public static final String BACKSLASH = StrPool.BACKSLASH;
/**
* 字符串常量:回车符 {@code "\r"} <br> 解释:该字符常用于表示 Linux 系统和 MacOS 系统下的文本换行
*/
public static final String CR = StrPool.CR;
/**
* 字符串常量:换行符 {@code "\n"}
*/
public static final String LF = StrPool.LF;
/**
* 字符串常量Windows 换行 {@code "\r\n"} <br> 解释:该字符串常用于表示 Windows 系统下的文本换行
*/
public static final String CRLF = StrPool.CRLF;
/**
* 字符串常量:下划线 {@code "_"}
*/
public static final String UNDERLINE = StrPool.UNDERLINE;
/**
* 字符串常量:减号(连接符) {@code "-"}
*/
public static final String DASHED = StrPool.DASHED;
/**
* 字符串常量:逗号 {@code ","}
*/
public static final String COMMA = StrPool.COMMA;
/**
* 字符串常量:花括号(左) <code>"{"</code>
*/
public static final String DELIM_START = StrPool.DELIM_START;
/**
* 字符串常量:花括号(右) <code>"}"</code>
*/
public static final String DELIM_END = StrPool.DELIM_END;
/**
* 字符串常量:中括号(左) {@code "["}
*/
public static final String BRACKET_START = StrPool.BRACKET_START;
/**
* 字符串常量:中括号(右) {@code "]"}
*/
public static final String BRACKET_END = StrPool.BRACKET_END;
/**
* 字符串常量:冒号 {@code ":"}
*/
public static final String COLON = StrPool.COLON;
/**
* 字符串常量:艾特 {@code "@"}
*/
public static final String AT = StrPool.AT;
/**
* 字符串常量HTML 不间断空格转义 {@code "&nbsp;" -> " "}
*/
public static final String HTML_NBSP = XmlUtil.NBSP;
/**
* 字符串常量HTML And 符转义 {@code "&amp;" -> "&"}
*/
public static final String HTML_AMP = XmlUtil.AMP;
/**
* 字符串常量HTML 双引号转义 {@code "&quot;" -> "\""}
*/
public static final String HTML_QUOTE = XmlUtil.QUOTE;
/**
* 字符串常量HTML 单引号转义 {@code "&apos" -> "'"}
*/
public static final String HTML_APOS = XmlUtil.APOS;
/**
* 字符串常量HTML 小于号转义 {@code "&lt;" -> "<"}
*/
public static final String HTML_LT = XmlUtil.LT;
/**
* 字符串常量HTML 大于号转义 {@code "&gt;" -> ">"}
*/
public static final String HTML_GT = XmlUtil.GT;
/**
* 字符串常量:空 JSON {@code "{}"}
*/
public static final String EMPTY_JSON = StrPool.EMPTY_JSON;
/**
* 空字符串
*/ */
public static final String EMPTY = ""; public static final String EMPTY = "";
/** /**
* 空格 * 空格符 {@code " "}
*/ */
public static final String SPACE = " "; public static final String SPACE = " ";
/** /**
* 分号 * 制表符 {@code "\t"}
*/
public static final String TAB = " ";
/**
* 空 JSON {@code "{}"}
*/
public static final String EMPTY_JSON = "{}";
/**
* 点 {@code "."}
*/
public static final String DOT = ".";
/**
* 双点 {@code ".."}
* <p>
* 作为指向上级文件夹的路径,如:{@code "../path"}
* </p>
*/
public static final String DOUBLE_DOT = "..";
/**
* 逗号 {@code ","}
*/
public static final String COMMA = ",";
/**
* 中文逗号 {@code ""}
*/
public static final String CHINESE_COMMA = "";
/**
* 冒号 {@code ":"}
*/
public static final String COLON = ":";
/**
* 分号 {@code ";"}
*/ */
public static final String SEMICOLON = ";"; public static final String SEMICOLON = ";";
/** /**
* 星号 * 问号 {@code "?"}
*/
public static final String ASTERISK = "*";
/**
* 问号
*/ */
public static final String QUESTION_MARK = "?"; public static final String QUESTION_MARK = "?";
/** /**
* 中文逗号 * 下划线 {@code "_"}
*/ */
public static final String CHINESE_COMMA = ""; public static final String UNDERLINE = "_";
/**
* 减号(连接符) {@code "-"}
*/
public static final String DASHED = "-";
/**
* 星号 {@code "*"}
*/
public static final String ASTERISK = "*";
/**
* 斜杠 {@code "/"}
*/
public static final String SLASH = "/";
/**
* 反斜杠 {@code "\\"}
*/
public static final String BACKSLASH = "\\";
/**
* 管道符 {@code "|"}
*/
public static final String PIPE = "|";
/**
* 艾特 {@code "@"}
*/
public static final String AT = "@";
/**
* 与符号 {@code "&"}
*/
public static final String AMP = "&";
/**
* 花括号(左) <code>"{"</code>
*/
public static final String DELIM_START = "{";
/**
* 花括号(右) <code>"}"</code>
*/
public static final String DELIM_END = "}";
/**
* 中括号(左) {@code "["}
*/
public static final String BRACKET_START = "[";
/**
* 中括号(右) {@code "]"}
*/
public static final String BRACKET_END = "]";
/** /**
* 圆括号(左) {@code "("} * 圆括号(左) {@code "("}
@@ -270,20 +155,70 @@ public class StringConstants {
public static final String ROUND_BRACKET_END = ")"; public static final String ROUND_BRACKET_END = ")";
/** /**
* 等号(= * 双引号 {@code "\""}
*/
public static final String DOUBLE_QUOTES = "\"";
/**
* 单引号 {@code "'"}
*/
public static final String SINGLE_QUOTE = "'";
/**
* 等号 {@code "="}
*/ */
public static final String EQUALS = "="; public static final String EQUALS = "=";
/** /**
* 路径模式 * 回车符 {@code "\r"}
*/
public static final String CR = "\r";
/**
* 换行符 {@code "\n"}
*/
public static final String LF = "\n";
/**
* 路径模式 {@code "/**"}
*/ */
public static final String PATH_PATTERN = "/**"; public static final String PATH_PATTERN = "/**";
/** /**
* 路径模式(仅匹配当前目录) * 路径模式(仅匹配当前目录) {@code "/*"}
*/ */
public static final String PATH_PATTERN_CURRENT_DIR = "/*"; public static final String PATH_PATTERN_CURRENT_DIR = "/*";
/**
* HTML 不间断空格转义 {@code "&nbsp;" -> " "}
*/
public static final String HTML_NBSP = "&nbsp;";
/**
* HTML And 符转义 {@code "&amp;" -> "&"}
*/
public static final String HTML_AMP = "&amp;";
/**
* HTML 双引号转义 {@code "&quot;" -> "\""}
*/
public static final String HTML_QUOTE = "&quot;";
/**
* HTML 单引号转义 {@code "&apos" -> "'"}
*/
public static final String HTML_APOS = "&apos;";
/**
* HTML 小于号转义 {@code "&lt;" -> "<"}
*/
public static final String HTML_LT = "&lt;";
/**
* HTML 大于号转义 {@code "&gt;" -> ">"}
*/
public static final String HTML_GT = "&gt;";
private StringConstants() { private StringConstants() {
} }
} }

View File

@@ -18,13 +18,13 @@ package top.continew.starter.core.util;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.NetUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HtmlUtil; import cn.hutool.http.HtmlUtil;
import net.dreamlu.mica.ip2region.core.Ip2regionSearcher; import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
import net.dreamlu.mica.ip2region.core.IpInfo; import net.dreamlu.mica.ip2region.core.IpInfo;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
@@ -50,12 +50,13 @@ public class IpUtils {
} }
Ip2regionSearcher ip2regionSearcher = SpringUtil.getBean(Ip2regionSearcher.class); Ip2regionSearcher ip2regionSearcher = SpringUtil.getBean(Ip2regionSearcher.class);
IpInfo ipInfo = ip2regionSearcher.memorySearch(ip); IpInfo ipInfo = ip2regionSearcher.memorySearch(ip);
if (null != ipInfo) { if (null == ipInfo) {
Set<String> regionSet = CollUtil.newLinkedHashSet(ipInfo.getAddress(), ipInfo.getIsp()); return null;
regionSet.removeIf(CharSequenceUtil::isBlank);
return String.join(StringConstants.SPACE, regionSet);
} }
return null; Set<String> regionSet = CollUtil.newLinkedHashSet(ipInfo.getCountry(), ipInfo.getRegion(), ipInfo
.getProvince(), ipInfo.getCity(), ipInfo.getIsp());
regionSet.removeIf(Objects::isNull);
return String.join(StringConstants.PIPE, regionSet);
} }
/** /**

View File

@@ -0,0 +1,204 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import java.util.function.BooleanSupplier;
/**
* 业务参数校验工具类(抛出 500 ServiceException
*
* @author Charles7c
* @see BusinessException
* @since 1.0.0
*/
public class CheckUtils extends Validator {
private static final Class<BusinessException> EXCEPTION_TYPE = BusinessException.class;
private CheckUtils() {
}
/**
* 如果不存在,抛出异常
*
* @param obj 被检测的对象
* @param entityName 实体名
* @param fieldName 字段名
* @param fieldValue 字段值
*/
public static void throwIfNotExists(Object obj, String entityName, String fieldName, Object fieldValue) {
String message = "%s 为 [%s] 的 %s 记录已不存在".formatted(fieldName, fieldValue, CharSequenceUtil
.replace(entityName, "DO", StringConstants.EMPTY));
throwIfNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNull(Object obj, String template, Object... params) {
throwIfNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotNull(Object obj, String template, Object... params) {
throwIfNotNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果存在,抛出异常
*
* @param obj 被检测的对象
* @param entityName 实体名
* @param fieldName 字段名
* @param fieldValue 字段值
*/
public static void throwIfExists(Object obj, String entityName, String fieldName, Object fieldValue) {
String message = "%s 为 [%s] 的 %s 记录已存在".formatted(fieldName, fieldValue, entityName);
throwIfNotNull(obj, message, EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEmpty(Object obj, String template, Object... params) {
throwIfEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEmpty(Object obj, String template, Object... params) {
throwIfNotEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param str 被检测的字符串
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfBlank(CharSequence str, String template, Object... params) {
throwIfBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param str 被检测的字符串
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotBlank(CharSequence str, String template, Object... params) {
throwIfNotBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEqual(Object obj1, Object obj2, String template, Object... params) {
throwIfEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEqual(Object obj1, Object obj2, String template, Object... params) {
throwIfNotEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String template, Object... params) {
throwIfEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEqualIgnoreCase(CharSequence str1,
CharSequence str2,
String template,
Object... params) {
throwIfNotEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
* @param condition 条件
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIf(boolean condition, String template, Object... params) {
throwIf(condition, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
* @param conditionSupplier 条件
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIf(BooleanSupplier conditionSupplier, String template, Object... params) {
throwIf(conditionSupplier, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation;
import cn.hutool.core.text.CharSequenceUtil;
import top.continew.starter.core.exception.BadRequestException;
import java.util.function.BooleanSupplier;
/**
* 基本参数校验工具类(抛出 400 BadRequestException
*
* @author Charles7c
* @see BadRequestException
* @since 1.0.0
*/
public class ValidationUtils extends Validator {
private static final Class<BadRequestException> EXCEPTION_TYPE = BadRequestException.class;
private ValidationUtils() {
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNull(Object obj, String template, Object... params) {
throwIfNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotNull(Object obj, String template, Object... params) {
throwIfNotNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEmpty(Object obj, String template, Object... params) {
throwIfEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEmpty(Object obj, String template, Object... params) {
throwIfNotEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果为空,抛出异常
*
* @param str 被检测的字符串
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfBlank(CharSequence str, String template, Object... params) {
throwIfBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不为空,抛出异常
*
* @param str 被检测的字符串
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotBlank(CharSequence str, String template, Object... params) {
throwIfNotBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEqual(Object obj1, Object obj2, String template, Object... params) {
throwIfEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEqual(Object obj1, Object obj2, String template, Object... params) {
throwIfNotEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String template, Object... params) {
throwIfEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIfNotEqualIgnoreCase(CharSequence str1,
CharSequence str2,
String template,
Object... params) {
throwIfNotEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
* @param condition 条件
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIf(boolean condition, String template, Object... params) {
throwIf(condition, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
/**
* 如果条件成立,抛出异常
*
* @param conditionSupplier 条件
* @param template 异常信息模板,被替换的部分用 {} 表示,如果模板为 null返回 "null"
* @param params 参数值
*/
public static void throwIf(BooleanSupplier conditionSupplier, String template, Object... params) {
throwIf(conditionSupplier, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import java.util.Set;
import java.util.function.BooleanSupplier;
/**
* 校验器
*
* @author Charles7c
* @since 1.0.0
*/
public class Validator {
private static final jakarta.validation.Validator VALIDATOR = SpringUtil
.getBean(jakarta.validation.Validator.class);
protected Validator() {
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
throwIf(null == obj, message, exceptionType);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
throwIf(null != obj, message, exceptionType);
}
/**
* 如果为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
throwIf(ObjectUtil.isEmpty(obj), message, exceptionType);
}
/**
* 如果不为空,抛出异常
*
* @param obj 被检测的对象
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
throwIf(ObjectUtil.isNotEmpty(obj), message, exceptionType);
}
/**
* 如果为空,抛出异常
*
* @param str 被检测的字符串
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfBlank(CharSequence str,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(CharSequenceUtil.isBlank(str), message, exceptionType);
}
/**
* 如果不为空,抛出异常
*
* @param str 被检测的字符串
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotBlank(CharSequence str,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(CharSequenceUtil.isNotBlank(str), message, exceptionType);
}
/**
* 如果相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfEqual(Object obj1,
Object obj2,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(ObjectUtil.equal(obj1, obj2), message, exceptionType);
}
/**
* 如果不相同,抛出异常
*
* @param obj1 要比较的对象1
* @param obj2 要比较的对象2
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEqual(Object obj1,
Object obj2,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(ObjectUtil.notEqual(obj1, obj2), message, exceptionType);
}
/**
* 如果相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfEqualIgnoreCase(CharSequence str1,
CharSequence str2,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(CharSequenceUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
}
/**
* 如果不相同,抛出异常(不区分大小写)
*
* @param str1 要比较的字符串1
* @param str2 要比较的字符串2
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIfNotEqualIgnoreCase(CharSequence str1,
CharSequence str2,
String message,
Class<? extends RuntimeException> exceptionType) {
throwIf(!CharSequenceUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
}
/**
* 如果条件成立,抛出异常
*
* @param condition 条件
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIf(boolean condition, String message, Class<? extends RuntimeException> exceptionType) {
if (condition) {
throw ReflectUtil.newInstance(exceptionType, message);
}
}
/**
* 如果条件成立,抛出异常
*
* @param conditionSupplier 条件
* @param message 错误信息
* @param exceptionType 异常类型
*/
protected static void throwIf(BooleanSupplier conditionSupplier,
String message,
Class<? extends RuntimeException> exceptionType) {
if (null != conditionSupplier && conditionSupplier.getAsBoolean()) {
throw ReflectUtil.newInstance(exceptionType, message);
}
}
/**
* JSR 303 校验
*
* @param obj 被校验对象
* @param groups 分组
* @since 2.3.0
*/
public static void validate(Object obj, Class<?>... groups) {
Set<ConstraintViolation<Object>> violations = VALIDATOR.validate(obj, groups);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation.constraints;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* 枚举校验注解
*
* <p>
* {@code @EnumValue(value = XxxEnum.class, message = "参数值非法")} <br />
* {@code @EnumValue(enumValues = {"F", "M"} ,message = "性别只允许为F或M")}
* </p>
*
* @author Jasmine
* @author Charles7c
* @since 2.7.3
*/
@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {
/**
* 枚举类
*
* @return 枚举类
*/
Class<? extends Enum> value() default Enum.class;
/**
* 枚举值
*
* @return 枚举值
*/
String[] enumValues() default {};
/**
* 获取枚举值的方法名
*
* @return 获取枚举值的方法名
*/
String method() default "";
/**
* 提示消息
*
* @return 提示消息
*/
String message() default "参数值非法";
/**
* 分组
*
* @return 分组
*/
Class<?>[] groups() default {};
/**
* 负载
*
* @return 负载
*/
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.core.validation.constraints;
import cn.hutool.core.text.CharSequenceUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
/**
* 枚举校验注解校验器
*
* @author Charles7c
* @author Jasmine
* @since 2.7.3
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private static final Logger log = LoggerFactory.getLogger(EnumValueValidator.class);
private Class<? extends Enum> enumClass;
private String[] enumValues;
private String enumMethod;
@Override
public void initialize(EnumValue enumValue) {
this.enumClass = enumValue.value();
this.enumValues = enumValue.enumValues();
this.enumMethod = enumValue.method();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
// 优先校验 enumValues
if (enumValues.length > 0) {
return Arrays.asList(enumValues).contains(value.toString());
}
Enum[] enumConstants = enumClass.getEnumConstants();
if (enumConstants.length == 0) {
return false;
}
if (CharSequenceUtil.isBlank(enumMethod)) {
return findEnumValue(enumConstants, Enum::toString, value.toString());
}
try {
// 枚举类指定了方法名,则调用指定方法获取枚举值
Method method = enumClass.getMethod(enumMethod);
for (Enum enumConstant : enumConstants) {
if (method.invoke(enumConstant).equals(value)) {
return true;
}
}
} catch (Exception e) {
log.error("An error occurred while validating the enum value, please check the @EnumValue parameter configuration.", e);
}
return false;
}
/**
* 遍历枚举类,判断是否包含指定值
*
* @param enumConstants 枚举类数组
* @param function 获取枚举值的函数
* @param value 待校验的值
* @return 是否包含指定值
*/
private boolean findEnumValue(Enum[] enumConstants, Function<Enum, Object> function, Object value) {
for (Enum enumConstant : enumConstants) {
if (function.apply(enumConstant).equals(value)) {
return true;
}
}
return false;
}
}

View File

@@ -16,18 +16,21 @@
package top.continew.starter.data.core.util; package top.continew.starter.data.core.util;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.convert.Convert;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.db.Db; import cn.hutool.db.DbRuntimeException;
import cn.hutool.db.Entity; import cn.hutool.db.DbUtil;
import cn.hutool.db.meta.Column; import cn.hutool.db.meta.Column;
import cn.hutool.db.meta.MetaUtil; import cn.hutool.db.meta.MetaUtil;
import cn.hutool.db.meta.Table;
import cn.hutool.db.meta.TableType;
import top.continew.starter.core.exception.BusinessException; import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.data.core.enums.DatabaseType; import top.continew.starter.data.core.enums.DatabaseType;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DatabaseMetaData; import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -80,7 +83,7 @@ public class MetaUtils {
* @param dataSource 数据源 * @param dataSource 数据源
* @return 表信息列表 * @return 表信息列表
*/ */
public static List<Table> getTables(DataSource dataSource) throws SQLException { public static List<Table> getTables(DataSource dataSource) {
return getTables(dataSource, null); return getTables(dataSource, null);
} }
@@ -90,27 +93,39 @@ public class MetaUtils {
* @param dataSource 数据源 * @param dataSource 数据源
* @param tableName 表名称 * @param tableName 表名称
* @return 表信息列表 * @return 表信息列表
* @author looly
* @since 2.7.2
*/ */
public static List<Table> getTables(DataSource dataSource, String tableName) throws SQLException { public static List<Table> getTables(DataSource dataSource, String tableName) {
String querySql = "SHOW TABLE STATUS"; List<Table> tables = new ArrayList<>();
List<Entity> tableEntityList; Connection conn = null;
Db db = Db.use(dataSource); try {
if (CharSequenceUtil.isNotBlank(tableName)) { conn = dataSource.getConnection();
tableEntityList = db.query("%s WHERE NAME = ?".formatted(querySql), tableName); String catalog = MetaUtil.getCatalog(conn);
} else { String schema = MetaUtil.getSchema(conn);
tableEntityList = db.query(querySql); final DatabaseMetaData metaData = conn.getMetaData();
try (final ResultSet rs = metaData.getTables(catalog, schema, tableName, Convert
.toStrArray(TableType.TABLE))) {
if (null != rs) {
String name;
while (rs.next()) {
name = rs.getString("TABLE_NAME");
if (CharSequenceUtil.isNotBlank(name)) {
final Table table = Table.create(name);
table.setCatalog(catalog);
table.setSchema(schema);
table.setComment(MetaUtil.getRemarks(metaData, catalog, schema, name));
tables.add(table);
}
}
}
}
return tables;
} catch (Exception e) {
throw new DbRuntimeException("Get tables error!", e);
} finally {
DbUtil.close(conn);
} }
List<Table> tableList = new ArrayList<>(tableEntityList.size());
for (Entity tableEntity : tableEntityList) {
Table table = new Table(tableEntity.getStr("NAME"));
table.setComment(tableEntity.getStr("COMMENT"));
table.setEngine(tableEntity.getStr("ENGINE"));
table.setCharset(tableEntity.getStr("COLLATION"));
table.setCreateTime(DateUtil.toLocalDateTime(tableEntity.getDate("CREATE_TIME")));
table.setUpdateTime(DateUtil.toLocalDateTime(tableEntity.getDate("UPDATE_TIME")));
tableList.add(table);
}
return tableList;
} }
/** /**
@@ -121,7 +136,7 @@ public class MetaUtils {
* @return 列信息列表 * @return 列信息列表
*/ */
public static Collection<Column> getColumns(DataSource dataSource, String tableName) { public static Collection<Column> getColumns(DataSource dataSource, String tableName) {
cn.hutool.db.meta.Table table = MetaUtil.getTableMeta(dataSource, tableName); Table table = MetaUtil.getTableMeta(dataSource, tableName);
return table.getColumns(); return table.getColumns();
} }
} }

View File

@@ -1,115 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.core.util;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 数据库表信息
*
* @author Charles7c
* @since 1.0.0
*/
public class Table implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 表名称
*/
private String tableName;
/**
* 注释
*/
private String comment;
/**
* 存储引擎
*/
private String engine;
/**
* 字符集
*/
private String charset;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改时间
*/
private LocalDateTime updateTime;
public Table(String tableName) {
this.tableName = tableName;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public String getEngine() {
return engine;
}
public void setEngine(String engine) {
this.engine = engine;
}
public String getCharset() {
return charset;
}
public void setCharset(String charset) {
this.charset = charset;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
}

View File

@@ -16,7 +16,7 @@
package top.continew.starter.data.mf.datapermission; package top.continew.starter.data.mf.datapermission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.text.CharSequenceUtil;
import com.mybatisflex.core.dialect.impl.CommonsDialectImpl; import com.mybatisflex.core.dialect.impl.CommonsDialectImpl;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
@@ -152,7 +152,7 @@ public class DataPermissionDialect extends CommonsDialectImpl {
* @return 带表别名字段 * @return 带表别名字段
*/ */
private String buildColumn(String tableAlias, String columnName) { private String buildColumn(String tableAlias, String columnName) {
if (StrUtil.isNotEmpty(tableAlias)) { if (CharSequenceUtil.isNotEmpty(tableAlias)) {
return "%s.%s".formatted(tableAlias, columnName); return "%s.%s".formatted(tableAlias, columnName);
} }
return columnName; return columnName;

View File

@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import top.continew.starter.core.exception.BadRequestException; import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.ReflectUtils; import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils; import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.data.core.annotation.Query; import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore; import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType; import top.continew.starter.data.core.enums.QueryType;
@@ -199,7 +199,9 @@ public class QueryWrapperHelper {
case LT -> consumers.add(q -> q.lt(columnName, fieldValue)); case LT -> consumers.add(q -> q.lt(columnName, fieldValue));
case LE -> consumers.add(q -> q.le(columnName, fieldValue)); case LE -> consumers.add(q -> q.le(columnName, fieldValue));
case BETWEEN -> { case BETWEEN -> {
List<Object> between = new ArrayList<>((List<Object>)fieldValue); List<Object> between = new ArrayList<>(ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (List<Object>)fieldValue);
ValidationUtils.throwIf(between.size() != 2, "[{}] 必须是一个范围", columnName); ValidationUtils.throwIf(between.size() != 2, "[{}] 必须是一个范围", columnName);
consumers.add(q -> q.between(columnName, between.get(0), between.get(1))); consumers.add(q -> q.between(columnName, between.get(0), between.get(1)));
} }
@@ -208,11 +210,15 @@ public class QueryWrapperHelper {
case LIKE_RIGHT -> consumers.add(q -> q.likeRight(columnName, fieldValue)); case LIKE_RIGHT -> consumers.add(q -> q.likeRight(columnName, fieldValue));
case IN -> { case IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName); ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.in(columnName, (Collection<Object>)fieldValue)); consumers.add(q -> q.in(columnName, ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (Collection<Object>)fieldValue));
} }
case NOT_IN -> { case NOT_IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName); ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.notIn(columnName, (Collection<Object>)fieldValue)); consumers.add(q -> q.notIn(columnName, ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (Collection<Object>)fieldValue));
} }
case IS_NULL -> consumers.add(q -> q.isNull(columnName)); case IS_NULL -> consumers.add(q -> q.isNull(columnName));
case IS_NOT_NULL -> consumers.add(q -> q.isNotNull(columnName)); case IS_NOT_NULL -> consumers.add(q -> q.isNotNull(columnName));

View File

@@ -36,7 +36,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
import top.continew.starter.core.constant.PropertiesConstants; import top.continew.starter.core.constant.PropertiesConstants;
import top.continew.starter.core.util.GeneralPropertySourceFactory; import top.continew.starter.core.util.GeneralPropertySourceFactory;
import top.continew.starter.data.mp.autoconfigure.idgenerator.MyBatisPlusIdGeneratorConfiguration; import top.continew.starter.data.mp.autoconfigure.idgenerator.MyBatisPlusIdGeneratorConfiguration;
import top.continew.starter.data.mp.handler.MybatisBaseEnumTypeHandler; import top.continew.starter.data.mp.handler.CompositeBaseEnumTypeHandler;
import java.util.Map; import java.util.Map;
@@ -63,7 +63,8 @@ public class MybatisPlusAutoConfiguration {
*/ */
@Bean @Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() { public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
return properties -> properties.getConfiguration().setDefaultEnumTypeHandler(MybatisBaseEnumTypeHandler.class); return properties -> properties.getConfiguration()
.setDefaultEnumTypeHandler(CompositeBaseEnumTypeHandler.class);
} }
/** /**
@@ -107,9 +108,8 @@ public class MybatisPlusAutoConfiguration {
*/ */
private PaginationInnerInterceptor paginationInnerInterceptor(MyBatisPlusExtensionProperties.PaginationProperties paginationProperties) { private PaginationInnerInterceptor paginationInnerInterceptor(MyBatisPlusExtensionProperties.PaginationProperties paginationProperties) {
// 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型 // 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型
PaginationInnerInterceptor paginationInnerInterceptor = null != paginationProperties.getDbType() PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(paginationProperties
? new PaginationInnerInterceptor(paginationProperties.getDbType()) .getDbType());
: new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(paginationProperties.isOverflow()); paginationInnerInterceptor.setOverflow(paginationProperties.isOverflow());
paginationInnerInterceptor.setMaxLimit(paginationProperties.getMaxLimit()); paginationInnerInterceptor.setMaxLimit(paginationProperties.getMaxLimit());
return paginationInnerInterceptor; return paginationInnerInterceptor;

View File

@@ -0,0 +1,105 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.mp.handler;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.apache.ibatis.type.EnumTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import java.lang.reflect.Constructor;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 复合枚举类型处理器(扩展 BaseEnum 支持)
*
* @author miemie<a href="https://gitee.com/baomidou/mybatis-plus">MyBatis Plus</a>
* @author Charles7c
* @see com.baomidou.mybatisplus.core.handlers.CompositeEnumTypeHandler
* @since 2.7.3
*/
public class CompositeBaseEnumTypeHandler<E extends Enum<E>> implements TypeHandler<E> {
private static final Map<Class<?>, Boolean> MP_ENUM_CACHE = new ConcurrentHashMap<>();
private static Class<? extends TypeHandler> defaultEnumTypeHandler = EnumTypeHandler.class;
private final TypeHandler<E> delegate;
public CompositeBaseEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
if (Boolean.TRUE.equals(CollectionUtils
.computeIfAbsent(MP_ENUM_CACHE, enumClassType, MybatisBaseEnumTypeHandler::isMpEnums))) {
delegate = new MybatisBaseEnumTypeHandler<>(enumClassType);
} else {
delegate = getInstance(enumClassType, defaultEnumTypeHandler);
}
}
public static void setDefaultEnumTypeHandler(Class<? extends TypeHandler> defaultEnumTypeHandler) {
if (defaultEnumTypeHandler != null && !MybatisBaseEnumTypeHandler.class
.isAssignableFrom(defaultEnumTypeHandler)) {
CompositeBaseEnumTypeHandler.defaultEnumTypeHandler = defaultEnumTypeHandler;
}
}
@Override
public void setParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
delegate.setParameter(ps, i, parameter, jdbcType);
}
@Override
public E getResult(ResultSet rs, String columnName) throws SQLException {
return delegate.getResult(rs, columnName);
}
@Override
public E getResult(ResultSet rs, int columnIndex) throws SQLException {
return delegate.getResult(rs, columnIndex);
}
@Override
public E getResult(CallableStatement cs, int columnIndex) throws SQLException {
return delegate.getResult(cs, columnIndex);
}
@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>)c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>)c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}
}

View File

@@ -43,10 +43,11 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* 自定义枚举属性转换器 * 枚举类型处理器(扩展 BaseEnum 支持)
* *
* @author hubin * @author hubin<a href="https://gitee.com/baomidou/mybatis-plus">MyBatis Plus</a>
* @author Charles7c * @author Charles7c
* @see com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
* @since 2.4.0 * @since 2.4.0
*/ */
public class MybatisBaseEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> { public class MybatisBaseEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {

View File

@@ -18,7 +18,7 @@ package top.continew.starter.data.mp.service.impl;
import cn.hutool.core.util.ClassUtil; import cn.hutool.core.util.ClassUtil;
import top.continew.starter.core.util.ReflectUtils; import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.CheckUtils; import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.data.mp.base.BaseMapper; import top.continew.starter.data.mp.base.BaseMapper;
import top.continew.starter.data.mp.service.IService; import top.continew.starter.data.mp.service.IService;

View File

@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import top.continew.starter.core.exception.BadRequestException; import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.ReflectUtils; import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils; import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.data.core.annotation.Query; import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore; import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType; import top.continew.starter.data.core.enums.QueryType;
@@ -193,7 +193,9 @@ public class QueryWrapperHelper {
case LT -> consumers.add(q -> q.lt(columnName, fieldValue)); case LT -> consumers.add(q -> q.lt(columnName, fieldValue));
case LE -> consumers.add(q -> q.le(columnName, fieldValue)); case LE -> consumers.add(q -> q.le(columnName, fieldValue));
case BETWEEN -> { case BETWEEN -> {
List<Object> between = new ArrayList<>((List<Object>)fieldValue); List<Object> between = new ArrayList<>(ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (List<Object>)fieldValue);
ValidationUtils.throwIf(between.size() != 2, "[{}] 必须是一个范围", columnName); ValidationUtils.throwIf(between.size() != 2, "[{}] 必须是一个范围", columnName);
consumers.add(q -> q.between(columnName, between.get(0), between.get(1))); consumers.add(q -> q.between(columnName, between.get(0), between.get(1)));
} }
@@ -202,11 +204,15 @@ public class QueryWrapperHelper {
case LIKE_RIGHT -> consumers.add(q -> q.likeRight(columnName, fieldValue)); case LIKE_RIGHT -> consumers.add(q -> q.likeRight(columnName, fieldValue));
case IN -> { case IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName); ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.in(columnName, (Collection<Object>)fieldValue)); consumers.add(q -> q.in(columnName, ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (Collection<Object>)fieldValue));
} }
case NOT_IN -> { case NOT_IN -> {
ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName); ValidationUtils.throwIfEmpty(fieldValue, "[{}] 不能为空", columnName);
consumers.add(q -> q.notIn(columnName, (Collection<Object>)fieldValue)); consumers.add(q -> q.notIn(columnName, ArrayUtil.isArray(fieldValue)
? CollUtil.toList(fieldValue)
: (Collection<Object>)fieldValue));
} }
case IS_NULL -> consumers.add(q -> q.isNull(columnName)); case IS_NULL -> consumers.add(q -> q.isNull(columnName));
case IS_NOT_NULL -> consumers.add(q -> q.isNotNull(columnName)); case IS_NOT_NULL -> consumers.add(q -> q.isNotNull(columnName));

View File

@@ -43,7 +43,7 @@
<properties> <properties>
<!-- 项目版本号 --> <!-- 项目版本号 -->
<revision>2.7.1</revision> <revision>2.7.3</revision>
<snail-job.version>1.1.2</snail-job.version> <snail-job.version>1.1.2</snail-job.version>
<sa-token.version>1.39.0</sa-token.version> <sa-token.version>1.39.0</sa-token.version>
<just-auth.version>1.16.6</just-auth.version> <just-auth.version>1.16.6</just-auth.version>
@@ -53,7 +53,7 @@
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<jetcache.version>2.7.6</jetcache.version> <jetcache.version>2.7.6</jetcache.version>
<redisson.version>3.36.0</redisson.version> <redisson.version>3.36.0</redisson.version>
<cosid.version>2.9.8</cosid.version> <cosid.version>2.9.9</cosid.version>
<sms4j.version>3.3.3</sms4j.version> <sms4j.version>3.3.3</sms4j.version>
<aj-captcha.version>1.3.0</aj-captcha.version> <aj-captcha.version>1.3.0</aj-captcha.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>

View File

@@ -39,5 +39,5 @@ public @interface CrudRequestMapping {
/** /**
* API 列表 * API 列表
*/ */
Api[] api() default {Api.PAGE, Api.GET, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}; Api[] api() default {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT};
} }

View File

@@ -42,4 +42,11 @@ public @interface DictField {
* @return 值字段名 * @return 值字段名
*/ */
String valueKey() default "id"; String valueKey() default "id";
/**
* 额外信息字段名
*
* @return 额外信息字段名
*/
String[] extraKeys() default {};
} }

View File

@@ -71,4 +71,11 @@ public @interface TreeField {
* @return 递归深度 * @return 递归深度
*/ */
int deep() default -1; int deep() default -1;
/**
* 根节点 ID
*
* @return 根节点 ID
*/
long rootId() default 0L;
} }

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import top.continew.starter.core.constant.PropertiesConstants;
/**
* CRUD 配置属性
*
* @author Charles7c
* @since 2.7.2
*/
@ConfigurationProperties(PropertiesConstants.CRUD)
public class CrudProperties {
/**
* 树配置
*/
@NestedConfigurationProperty
private CrudTreeProperties tree = new CrudTreeProperties();
public CrudTreeProperties getTree() {
return tree;
}
public void setTree(CrudTreeProperties tree) {
this.tree = tree;
}
}

View File

@@ -20,6 +20,7 @@ import jakarta.annotation.PostConstruct;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
@@ -36,6 +37,7 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider;
* @since 1.0.0 * @since 1.0.0
*/ */
@Configuration @Configuration
@EnableConfigurationProperties(CrudProperties.class)
public class CrudRestControllerAutoConfiguration extends DelegatingWebMvcConfiguration { public class CrudRestControllerAutoConfiguration extends DelegatingWebMvcConfiguration {
private static final Logger log = LoggerFactory.getLogger(CrudRestControllerAutoConfiguration.class); private static final Logger log = LoggerFactory.getLogger(CrudRestControllerAutoConfiguration.class);

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.autoconfigure;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import top.continew.starter.core.util.validate.CheckUtils;
import top.continew.starter.extension.crud.annotation.TreeField;
/**
* CRUD 树列表配置属性
*
* @author Charles7c
* @since 2.7.2
*/
public class CrudTreeProperties {
/**
* ID 字段名
*/
private String idKey = "id";
/**
* 父 ID 字段名
*
*/
private String parentIdKey = "parentId";
/**
* 名称字段名
*
*/
private String nameKey = "name";
/**
* 排序字段名
*
*/
private String weightKey = "weight";
/**
* 子列表字段名
*
*/
private String childrenKey = "children";
/**
* 递归深度(< 0 不限制)
*/
private Integer deep = -1;
/**
* 根节点 ID
*/
private Long rootId = 0L;
public String getIdKey() {
return idKey;
}
public void setIdKey(String idKey) {
this.idKey = idKey;
}
public String getParentIdKey() {
return parentIdKey;
}
public void setParentIdKey(String parentIdKey) {
this.parentIdKey = parentIdKey;
}
public String getNameKey() {
return nameKey;
}
public void setNameKey(String nameKey) {
this.nameKey = nameKey;
}
public String getWeightKey() {
return weightKey;
}
public void setWeightKey(String weightKey) {
this.weightKey = weightKey;
}
public String getChildrenKey() {
return childrenKey;
}
public void setChildrenKey(String childrenKey) {
this.childrenKey = childrenKey;
}
public Integer getDeep() {
return deep;
}
public void setDeep(Integer deep) {
this.deep = deep;
}
public Long getRootId() {
return rootId;
}
public void setRootId(Long rootId) {
this.rootId = rootId;
}
/**
* 生成 {@link TreeNodeConfig} 对象
*
* @return {@link TreeNodeConfig} 对象
*/
public TreeNodeConfig genTreeNodeConfig() {
return TreeNodeConfig.DEFAULT_CONFIG.setIdKey(idKey)
.setParentIdKey(parentIdKey)
.setNameKey(nameKey)
.setWeightKey(weightKey)
.setChildrenKey(childrenKey)
.setDeep(deep < 0 ? null : deep);
}
/**
* 根据 @TreeField 配置生成树结构配置
*
* @param treeField 树结构字段注解
* @return 树结构配置
*/
public TreeNodeConfig genTreeNodeConfig(TreeField treeField) {
CheckUtils.throwIfNull(treeField, "请添加并配置 @TreeField 树结构信息");
return new TreeNodeConfig().setIdKey(treeField.value())
.setParentIdKey(treeField.parentIdKey())
.setNameKey(treeField.nameKey())
.setWeightKey(treeField.weightKey())
.setChildrenKey(treeField.childrenKey())
.setDeep(treeField.deep() < 0 ? null : treeField.deep());
}
}

View File

@@ -28,34 +28,42 @@ public enum Api {
* 所有 API * 所有 API
*/ */
ALL, ALL,
/** /**
* 分页 * 分页
*/ */
PAGE, PAGE,
/**
* 树列表
*/
TREE,
/** /**
* 列表 * 列表
*/ */
LIST, LIST,
/**
* 树列表
*/
TREE,
/** /**
* 详情 * 详情
*/ */
GET, DETAIL,
/** /**
* 新增 * 新增
*/ */
ADD, ADD,
/** /**
* 修改 * 修改
*/ */
UPDATE, UPDATE,
/** /**
* 删除 * 删除
*/ */
DELETE, DELETE,
/** /**
* 导出 * 导出
*/ */

View File

@@ -54,11 +54,11 @@ public class LabelValueResp<T> implements Serializable {
private Boolean disabled; private Boolean disabled;
/** /**
* 扩展 * 额外数据
*/ */
@Schema(description = "扩展") @Schema(description = "额外数据")
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Object extend; private Object extra;
public LabelValueResp() { public LabelValueResp() {
} }
@@ -68,10 +68,10 @@ public class LabelValueResp<T> implements Serializable {
this.value = value; this.value = value;
} }
public LabelValueResp(String label, T value, Object extend) { public LabelValueResp(String label, T value, Object extra) {
this.label = label; this.label = label;
this.value = value; this.value = value;
this.extend = extend; this.extra = extra;
} }
public LabelValueResp(String label, T value, Boolean disabled) { public LabelValueResp(String label, T value, Boolean disabled) {
@@ -104,11 +104,11 @@ public class LabelValueResp<T> implements Serializable {
this.disabled = disabled; this.disabled = disabled;
} }
public Object getExtend() { public Object getExtra() {
return extend; return extra;
} }
public void setExtend(Object extend) { public void setExtra(Object extra) {
this.extend = extend; this.extra = extra;
} }
} }

View File

@@ -1,95 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.lang.tree.parser.NodeParser;
import cn.hutool.core.util.ReflectUtil;
import top.continew.starter.core.util.validate.CheckUtils;
import top.continew.starter.extension.crud.annotation.TreeField;
import java.util.ArrayList;
import java.util.List;
/**
* 树工具类
*
* @author Charles7c
* @since 1.0.0
*/
public class TreeUtils {
/**
* 默认字段配置对象(根据前端树结构灵活调整名称)
*/
public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("title")
.setIdKey("key")
.setWeightKey("sort");
private TreeUtils() {
}
/**
* 树构建
*
* @param <T> 转换的实体 为数据源里的对象类型
* @param <E> ID类型
* @param list 源数据集合
* @param nodeParser 转换器
* @return List 树列表
*/
public static <T, E> List<Tree<E>> build(List<T> list, NodeParser<T, E> nodeParser) {
return build(list, DEFAULT_CONFIG, nodeParser);
}
/**
* 树构建
*
* @param <T> 转换的实体 为数据源里的对象类型
* @param <E> ID类型
* @param list 源数据集合
* @param treeNodeConfig 配置
* @param nodeParser 转换器
* @return List 树列表
*/
public static <T, E> List<Tree<E>> build(List<T> list, TreeNodeConfig treeNodeConfig, NodeParser<T, E> nodeParser) {
if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0);
}
E parentId = (E)ReflectUtil.getFieldValue(list.get(0), treeNodeConfig.getParentIdKey());
return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser);
}
/**
* 根据 @TreeField 配置生成树结构配置
*
* @param treeField 树结构字段注解
* @return 树结构配置
*/
public static TreeNodeConfig genTreeNodeConfig(TreeField treeField) {
CheckUtils.throwIfNull(treeField, "请添加并配置 @TreeField 树结构信息");
return new TreeNodeConfig().setIdKey(treeField.value())
.setParentIdKey(treeField.parentIdKey())
.setNameKey(treeField.nameKey())
.setWeightKey(treeField.weightKey())
.setChildrenKey(treeField.childrenKey())
.setDeep(treeField.deep() < 0 ? null : treeField.deep());
}
}

View File

@@ -71,21 +71,6 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
return baseService.page(query, pageQuery); return baseService.page(query, pageQuery);
} }
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public List<Tree<Long>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.tree(query, sortQuery, false);
}
/** /**
* 查询列表 * 查询列表
* *
@@ -101,6 +86,21 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
return baseService.list(query, sortQuery); return baseService.list(query, sortQuery);
} }
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public List<Tree<Long>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.tree(query, sortQuery, false);
}
/** /**
* 查询详情 * 查询详情
* *
@@ -111,8 +111,8 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody @ResponseBody
@GetMapping("/{id}") @GetMapping("/{id}")
public D get(@PathVariable("id") Long id) { public D detail(@PathVariable("id") Long id) {
this.checkPermission(Api.LIST); this.checkPermission(Api.DETAIL);
return baseService.get(id); return baseService.get(id);
} }

View File

@@ -46,16 +46,6 @@ public interface BaseService<L, D, Q, C> {
*/ */
PageResp<L> page(Q query, PageQuery pageQuery); PageResp<L> page(Q query, PageQuery pageQuery);
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段)
* @return 树列表信息
*/
List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple);
/** /**
* 查询列表 * 查询列表
* *
@@ -65,6 +55,20 @@ public interface BaseService<L, D, Q, C> {
*/ */
List<L> list(Q query, SortQuery sortQuery); List<L> list(Q query, SortQuery sortQuery);
/**
* 查询树列表
* <p>
* 虽然提供了查询条件,但不建议使用,容易因缺失根节点导致树节点丢失。
* 建议在前端进行查询过滤,如需使用建议重写方法。
* </p>
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @DictField 局部配置)
* @return 树列表信息
*/
List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple);
/** /**
* 查看详情 * 查看详情
* *

View File

@@ -22,12 +22,14 @@ import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.StringConstants;
@@ -37,12 +39,13 @@ import top.continew.starter.data.mf.base.BaseMapper;
import top.continew.starter.data.mf.service.impl.ServiceImpl; import top.continew.starter.data.mf.service.impl.ServiceImpl;
import top.continew.starter.data.mf.util.QueryWrapperHelper; import top.continew.starter.data.mf.util.QueryWrapperHelper;
import top.continew.starter.extension.crud.annotation.TreeField; import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
import top.continew.starter.extension.crud.model.entity.BaseIdDO; import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService; import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.TreeUtils;
import top.continew.starter.file.excel.util.ExcelUtils; import top.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@@ -79,26 +82,39 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
return pageResp; return pageResp;
} }
@Override
public List<L> list(Q query, SortQuery sortQuery) {
List<L> list = this.list(query, sortQuery, listClass);
list.forEach(this::fill);
return list;
}
@Override @Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) { public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) {
List<L> list = this.list(query, sortQuery); List<L> list = this.list(query, sortQuery);
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0); return new ArrayList<>(0);
} }
// 如果构建简单树结构,则不包含基本树结构之外的扩展字段 CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
TreeNodeConfig treeNodeConfig = TreeUtils.DEFAULT_CONFIG; CrudTreeProperties treeProperties = crudProperties.getTree();
TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class); TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class);
if (!isSimple) { TreeNodeConfig treeNodeConfig;
// 根据 @TreeField 配置生成树结构配置 Long rootId;
treeNodeConfig = TreeUtils.genTreeNodeConfig(treeField); // 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置
if (isSimple) {
treeNodeConfig = treeProperties.genTreeNodeConfig();
rootId = treeProperties.getRootId();
} else {
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
rootId = treeField.rootId();
} }
// 构建树 // 构建树
return TreeUtils.build(list, treeNodeConfig, (node, tree) -> { return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> {
// 转换器
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value()))); tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey()))); tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey()))); tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey()))); tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
// 如果构建简单树结构,则不包含扩展字段
if (!isSimple) { if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass); List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
@@ -109,13 +125,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
}); });
} }
@Override
public List<L> list(Q query, SortQuery sortQuery) {
List<L> list = this.list(query, sortQuery, listClass);
list.forEach(this::fill);
return list;
}
@Override @Override
public D get(Long id) { public D get(Long id) {
T entity = super.getById(id); T entity = super.getById(id);

View File

@@ -71,21 +71,6 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
return baseService.page(query, pageQuery); return baseService.page(query, pageQuery);
} }
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public List<Tree<Long>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.tree(query, sortQuery, false);
}
/** /**
* 查询列表 * 查询列表
* *
@@ -101,6 +86,21 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
return baseService.list(query, sortQuery); return baseService.list(query, sortQuery);
} }
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @return 树列表信息
*/
@Operation(summary = "查询树列表", description = "查询树列表")
@ResponseBody
@GetMapping("/tree")
public List<Tree<Long>> tree(Q query, SortQuery sortQuery) {
this.checkPermission(Api.LIST);
return baseService.tree(query, sortQuery, false);
}
/** /**
* 查询详情 * 查询详情
* *
@@ -111,8 +111,8 @@ public abstract class BaseController<S extends BaseService<L, D, Q, C>, L, D, Q,
@Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH)
@ResponseBody @ResponseBody
@GetMapping("/{id}") @GetMapping("/{id}")
public D get(@PathVariable("id") Long id) { public D detail(@PathVariable("id") Long id) {
this.checkPermission(Api.LIST); this.checkPermission(Api.DETAIL);
return baseService.get(id); return baseService.get(id);
} }

View File

@@ -46,16 +46,6 @@ public interface BaseService<L, D, Q, C> {
*/ */
PageResp<L> page(Q query, PageQuery pageQuery); PageResp<L> page(Q query, PageQuery pageQuery);
/**
* 查询树列表
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段)
* @return 树列表信息
*/
List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple);
/** /**
* 查询列表 * 查询列表
* *
@@ -65,6 +55,20 @@ public interface BaseService<L, D, Q, C> {
*/ */
List<L> list(Q query, SortQuery sortQuery); List<L> list(Q query, SortQuery sortQuery);
/**
* 查询树列表
* <p>
* 虽然提供了查询条件,但不建议使用,容易因缺失根节点导致树节点丢失。
* 建议在前端进行查询过滤,如需使用建议重写方法。
* </p>
*
* @param query 查询条件
* @param sortQuery 排序查询条件
* @param isSimple 是否为简单树结构(不包含基本树结构之外的扩展字段,简单树(下拉列表)使用全局配置结构,复杂树(表格)使用 @DictField 局部配置)
* @return 树列表信息
*/
List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple);
/** /**
* 查询详情 * 查询详情
* *

View File

@@ -20,8 +20,10 @@ import cn.crane4j.core.support.OperateTemplate;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions; import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig; import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
@@ -42,20 +44,18 @@ import top.continew.starter.data.mp.service.impl.ServiceImpl;
import top.continew.starter.data.mp.util.QueryWrapperHelper; import top.continew.starter.data.mp.util.QueryWrapperHelper;
import top.continew.starter.extension.crud.annotation.DictField; import top.continew.starter.extension.crud.annotation.DictField;
import top.continew.starter.extension.crud.annotation.TreeField; import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.autoconfigure.CrudProperties;
import top.continew.starter.extension.crud.autoconfigure.CrudTreeProperties;
import top.continew.starter.extension.crud.model.entity.BaseIdDO; import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp; import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService; import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.TreeUtils;
import top.continew.starter.file.excel.util.ExcelUtils; import top.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/** /**
* 业务实现基类 * 业务实现基类
@@ -86,28 +86,41 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
return pageResp; return pageResp;
} }
@Override
public List<L> list(Q query, SortQuery sortQuery) {
List<L> list = this.list(query, sortQuery, this.getListClass());
list.forEach(this::fill);
return list;
}
@Override @Override
public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) { public List<Tree<Long>> tree(Q query, SortQuery sortQuery, boolean isSimple) {
List<L> list = this.list(query, sortQuery); List<L> list = this.list(query, sortQuery);
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
return new ArrayList<>(0); return new ArrayList<>(0);
} }
// 如果构建简单树结构,则不包含基本树结构之外的扩展字段 CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class);
TreeNodeConfig treeNodeConfig = TreeUtils.DEFAULT_CONFIG; CrudTreeProperties treeProperties = crudProperties.getTree();
TreeField treeField = this.getListClass().getDeclaredAnnotation(TreeField.class); TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class);
if (!isSimple) { TreeNodeConfig treeNodeConfig;
// 根据 @TreeField 配置生成树结构配置 Long rootId;
treeNodeConfig = TreeUtils.genTreeNodeConfig(treeField); // 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置
if (isSimple) {
treeNodeConfig = treeProperties.genTreeNodeConfig();
rootId = treeProperties.getRootId();
} else {
treeNodeConfig = treeProperties.genTreeNodeConfig(treeField);
rootId = treeField.rootId();
} }
// 构建树 // 构建树
return TreeUtils.build(list, treeNodeConfig, (node, tree) -> { return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> {
// 转换器
tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value()))); tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value())));
tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey()))); tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey())));
tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey()))); tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey())));
tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey()))); tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey())));
// 如果构建简单树结构,则不包含扩展字段
if (!isSimple) { if (!isSimple) {
List<Field> fieldList = ReflectUtils.getNonStaticFields(this.getListClass()); List<Field> fieldList = ReflectUtils.getNonStaticFields(listClass);
fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField fieldList.removeIf(f -> CharSequenceUtil.equalsAnyIgnoreCase(f.getName(), treeField.value(), treeField
.parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey())); .parentIdKey(), treeField.nameKey(), treeField.weightKey(), treeField.childrenKey()));
fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f fieldList.forEach(f -> tree.putExtra(f.getName(), ReflectUtil.invoke(node, CharSequenceUtil.genGetter(f
@@ -116,13 +129,6 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
}); });
} }
@Override
public List<L> list(Q query, SortQuery sortQuery) {
List<L> list = this.list(query, sortQuery, this.getListClass());
list.forEach(this::fill);
return list;
}
@Override @Override
public D get(Long id) { public D get(Long id) {
T entity = super.getById(id, false); T entity = super.getById(id, false);
@@ -133,19 +139,40 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override @Override
public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) { public List<LabelValueResp> listDict(Q query, SortQuery sortQuery) {
QueryWrapper<T> queryWrapper = this.buildQueryWrapper(query);
this.sort(queryWrapper, sortQuery);
DictField dictField = super.getEntityClass().getDeclaredAnnotation(DictField.class); DictField dictField = super.getEntityClass().getDeclaredAnnotation(DictField.class);
CheckUtils.throwIfNull(dictField, "请添加并配置 @DictField 字典结构信息"); CheckUtils.throwIfNull(dictField, "请添加并配置 @DictField 字典结构信息");
// 指定查询字典字段 List<L> list = this.list(query, sortQuery);
queryWrapper.select(dictField.labelKey(), dictField.valueKey());
List<T> entityList = baseMapper.selectList(queryWrapper);
// 解析映射 // 解析映射
Map<String, String> fieldMapping = MapUtil.newHashMap(2); List<LabelValueResp> respList = new ArrayList<>(list.size());
fieldMapping.put(CharSequenceUtil.toCamelCase(dictField.labelKey()), "label"); String labelKey = dictField.labelKey().contains(StringConstants.DOT)
fieldMapping.put(CharSequenceUtil.toCamelCase(dictField.valueKey()), "value"); ? CharSequenceUtil.subAfter(dictField.labelKey(), StringConstants.DOT, true)
return BeanUtil.copyToList(entityList, LabelValueResp.class, CopyOptions.create() : dictField.labelKey();
.setFieldMapping(fieldMapping)); String valueKey = dictField.valueKey().contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(dictField.valueKey(), StringConstants.DOT, true)
: dictField.valueKey();
List<String> extraFieldNames = Arrays.stream(dictField.extraKeys())
.map(extraKey -> extraKey.contains(StringConstants.DOT)
? CharSequenceUtil.subAfter(extraKey, StringConstants.DOT, true)
: extraKey)
.map(CharSequenceUtil::toCamelCase)
.toList();
for (L entity : list) {
LabelValueResp<Object> labelValueResp = new LabelValueResp<>();
labelValueResp.setLabel(Convert.toStr(ReflectUtil.getFieldValue(entity, CharSequenceUtil
.toCamelCase(labelKey))));
labelValueResp.setValue(ReflectUtil.getFieldValue(entity, CharSequenceUtil.toCamelCase(valueKey)));
respList.add(labelValueResp);
if (CollUtil.isEmpty(extraFieldNames)) {
continue;
}
// 额外数据
Map<String, Object> extraMap = MapUtil.newHashMap(dictField.extraKeys().length);
for (String extraFieldName : extraFieldNames) {
extraMap.put(extraFieldName, ReflectUtil.getFieldValue(entity, extraFieldName));
}
labelValueResp.setExtra(extraMap);
}
return respList;
} }
@Override @Override

View File

@@ -18,7 +18,7 @@ package top.continew.starter.security.mask.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.constant.CharConstants;
import top.continew.starter.security.mask.core.JsonMaskSerializer; import top.continew.starter.security.mask.core.JsonMaskSerializer;
import top.continew.starter.security.mask.enums.MaskType; import top.continew.starter.security.mask.enums.MaskType;
import top.continew.starter.security.mask.strategy.IMaskStrategy; import top.continew.starter.security.mask.strategy.IMaskStrategy;
@@ -72,5 +72,5 @@ public @interface JsonMask {
/** /**
* 脱敏符号(默认:* * 脱敏符号(默认:*
*/ */
char character() default StringConstants.C_ASTERISK; char character() default CharConstants.ASTERISK;
} }