Compare commits

...

28 Commits

Author SHA1 Message Date
25dd7a5eb4 add RawMapper 2026-01-05 14:55:58 +08:00
430921a16c fix BaseMapper.NOT_DELETE invalid 2026-01-05 12:59:52 +08:00
77f9feb1a1 remove not usage length 2026-01-05 11:56:56 +08:00
a9156e07f4 fix javadoc warning 2026-01-04 17:27:36 +08:00
ec7f4ecaa9 update timi-io to 0.0.2 2026-01-04 17:27:27 +08:00
85009ccd5f ignored AI Agent prompt 2026-01-04 16:44:31 +08:00
e0c0db1c76 remove CLAUDE.md 2026-01-04 16:43:51 +08:00
edfbbcf11b rename BaseMapper.list* to select 2026-01-04 15:51:40 +08:00
4c1cdf0a91 fix SQLProvider.updateSelective update empty 2026-01-04 12:11:24 +08:00
1508bf7c7f fix SQLProvider example 2025-12-25 18:09:31 +08:00
e0398b3a22 rename likeExample to likesExample 2025-12-25 14:57:49 +08:00
1205946381 implements Updatable, Deletable 2025-12-24 11:22:42 +08:00
5fe610120b fix getLanguage NPE 2025-12-22 10:52:45 +08:00
4f0d2a380b add PageIgnore 2025-12-22 10:32:36 +08:00
c463ac5443 upper base lang field to timi-java for multilingual 2025-12-22 10:32:25 +08:00
d3aded669b allow update createdAt and deletedAt 2025-12-09 22:19:07 +08:00
413f376a15 fix pageExample 2025-12-09 10:21:41 +08:00
7a52560779 update Language.Enum 2025-12-08 16:57:03 +08:00
75c8f556a8 support equals or like Example for page 2025-12-08 16:56:41 +08:00
7654c3a360 ignored .claude 2025-12-08 16:54:54 +08:00
5239b469ac ignored illegal time for create and update 2025-12-08 16:07:31 +08:00
511b519925 fix SQLProvider count and page 2025-12-03 14:39:26 +08:00
595ca407b3 add CLAUDE.md prompt 2025-12-03 10:41:10 +08:00
7aadec7306 update BaseMapper.page 2025-12-03 10:40:50 +08:00
745b3acfef add BaseMapper.deleteAllByExample 2025-12-01 11:13:36 +08:00
23598242f0 improve SQLProvider selective filter 2025-12-01 11:00:03 +08:00
113af72208 add default implement for isDeleted() 2025-11-21 14:37:55 +08:00
17b20f38e6 fix SQLProvider.updateSelective 2025-11-18 15:10:46 +08:00
39 changed files with 1070 additions and 191 deletions

3
.gitignore vendored
View File

@ -57,3 +57,6 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.claude
/CLAUDE.md
/AGENTS.md

View File

@ -145,7 +145,7 @@
<dependency>
<groupId>com.imyeyu.io</groupId>
<artifactId>timi-io</artifactId>
<version>0.0.1</version>
<version>0.0.2</version>
</dependency>
</dependencies>
</project>

View File

@ -42,6 +42,12 @@ public class TimiSpring {
private static final Logger log = LoggerFactory.getLogger(TimiSpring.class);
private static final Gson GSON = new Gson();
/**
* 工具类禁止实例化
*/
private TimiSpring() {
}
/**
* 回调数据
*
@ -102,24 +108,50 @@ public class TimiSpring {
return getServletRequestAttributes().getRequest();
}
/**
* 获取请求域名
*
* @return 请求域名
*/
public static String getDomain() {
return getRequest().getServerName();
}
/**
* 获取完整域名(含协议与端口)
*
* @return 完整域名
*/
public static String getFullDomain() {
HttpServletRequest req = getRequest();
String port = req.getServerPort() == 80 || req.getServerPort() == 443 ? "" : ":" + req.getServerPort();
return "%s://%s%s".formatted(req.getScheme(), getDomain(), port);
}
/**
* 获取请求 URL
*
* @return 请求 URL
*/
public static String getURL() {
return getRequest().getRequestURL().toString();
}
/**
* 获取请求 URI
*
* @return 请求 URI
*/
public static String getURI() {
return getRequest().getRequestURI();
}
/**
* 从 URI 指定标记开始截取
*
* @param flag 标记
* @return 截取后的 URI
*/
public static String cutURIStartAt(String flag) {
int indexOf = getURI().indexOf(flag);
TimiException.requiredTrue(-1 < indexOf, "not found flag: %s".formatted(flag));
@ -315,14 +347,31 @@ public class TimiSpring {
return getRequest().getParameterValues(key);
}
/**
* 添加 Cookie
*
* @param cookie Cookie
*/
public static void addCookie(Cookie cookie) {
getResponse().addCookie(cookie);
}
/**
* 添加 Cookie
*
* @param key 键
* @param value 值
*/
public static void addCookie(String key, String value) {
addCookie(new Cookie(key, value));
}
/**
* 获取 Cookie
*
* @param key 键
* @return Cookie
*/
public static Cookie getCookie(String key) {
Cookie[] cookies = getRequest().getCookies();
if (cookies == null) {
@ -345,18 +394,27 @@ public class TimiSpring {
return TimiJava.firstNotEmpty(getHeader("Token"), getHeader("token"), getRequestArg("token"), getRequestArg("Token"));
}
/**
* 获取原始语言头
*
* @return 语言头
*/
public static String getLanguageRaw() {
return getHeader("Accept-Language");
}
/**
* 获取客户端地区语言
*
* @return 客户端地区语言
*/
public static Language getLanguage() {
public static Language.Enum getLanguage() {
String name = getRequestArg("lang");
if (TimiJava.isEmpty(name)) {
List<Locale.LanguageRange> rangeList = Locale.LanguageRange.parse(getLanguageRaw());
name = getLanguageRaw();
}
if (TimiJava.isNotEmpty(name)) {
List<Locale.LanguageRange> rangeList = Locale.LanguageRange.parse(name);
for (Locale.LanguageRange item : rangeList) {
if (item.getRange().contains("-")) {
name = item.getRange();
@ -368,9 +426,9 @@ public class TimiSpring {
name = name.replace("-", "_");
}
if (TimiJava.isEmpty(name)) { // use for not support
return Language.zh_CN;
return Language.Enum.zh_CN;
}
return Ref.toType(Language.class, name);
return Ref.toType(Language.Enum.class, name);
}
/**
@ -396,10 +454,22 @@ public class TimiSpring {
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
/**
* 是否本地 IP
*
* @return true 为本地 IP
*/
public static boolean isLocalIP() {
return getRequestIP().startsWith("127");
}
/**
* 解析 Range 请求范围
*
* @param fileLength 文件长度
* @return 请求范围
* @throws IOException IO 异常
*/
public static RequestRange requestRange(long fileLength) throws IOException {
HttpServletResponse resp = getResponse();

View File

@ -26,10 +26,17 @@ import org.springframework.stereotype.Component;
@Component
public class AOPLogInterceptor {
/** 全局请求追踪 ID Key */
public static final String REQUEST_ID = "TIMI_SPRING_REQUEST_ID";
private static final Logger log = LoggerFactory.getLogger(AOPLogInterceptor.class);
/**
* 创建 AOP 日志拦截器
*/
public AOPLogInterceptor() {
}
/** 注入注解 */
@Pointcut("@annotation(annotation.com.imyeyu.spring.AOPLog)")
public void logPointCut() {

View File

@ -18,6 +18,12 @@ public abstract class CaptchaValidAbstractInterceptor {
private boolean enable = true;
/**
* 创建验证码校验拦截器
*/
protected CaptchaValidAbstractInterceptor() {
}
/** 注入注解 */
@Pointcut("@annotation(com.imyeyu.spring.annotation.CaptchaValid)")
public void captchaPointCut() {
@ -45,12 +51,20 @@ public abstract class CaptchaValidAbstractInterceptor {
}
}
/**
* 校验验证码
*
* @param captchaId 验证码 ID
* @param captcha 验证码
*/
protected abstract void verify(String captchaId, String captcha);
/** 启用校验 */
public void enable() {
enable = true;
}
/** 禁用校验 */
public void disable() {
enable = false;
}

View File

@ -14,6 +14,18 @@ import org.springframework.web.servlet.HandlerInterceptor;
*/
public abstract class RequestRateLimitAbstractInterceptor implements HandlerInterceptor {
/**
* 创建访问频率限制拦截器
*/
protected RequestRateLimitAbstractInterceptor() {
}
/**
* 构建接口标识
*
* @param handlerMethod 方法信息
* @return 接口标识
*/
protected String buildId(HandlerMethod handlerMethod) {
return handlerMethod.getMethod().getDeclaringClass().getSimpleName() + "." + handlerMethod.getMethod().getName();
}

View File

@ -15,9 +15,21 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* 单参数请求解析器
*
* @author 夜雨
* @since 2025-10-13 16:29
*/
@Component
public class RequestSingleParamResolver implements HandlerMethodArgumentResolver {
/**
* 创建单参数解析器
*/
public RequestSingleParamResolver() {
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestSingleParam.class);

View File

@ -11,6 +11,7 @@ import java.lang.annotation.Annotation;
/**
* 抽象验证令牌
*
* @param <A> 注解类型
* @author 夜雨
* @version 2021-08-16 18:07
*/
@ -18,6 +19,11 @@ public abstract class RequiredTokenAbstractInterceptor<A extends Annotation> imp
private final Class<A> annotation;
/**
* 创建 Token 验证拦截器
*
* @param annotation 注解类型
*/
public RequiredTokenAbstractInterceptor(Class<A> annotation) {
this.annotation = annotation;
}

View File

@ -17,5 +17,10 @@ import java.lang.annotation.Target;
@Target(ElementType.FIELD)
public @interface Column {
/**
* 指定列名
*
* @return 列名
*/
String value();
}

View File

@ -0,0 +1,42 @@
package com.imyeyu.spring.annotation.table;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记逻辑删除字段并指定存储类型
*
* @author 夜雨
* @since 2025-12-01 10:56
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DeleteColumn {
/**
* 逻辑删除的存储类型
*
* @return 存储类型
*/
Type value() default Type.UNIX;
/**
* 逻辑删除的时间类型
*
* @author 夜雨
* @since 2025-12-01 10:57
*/
enum Type {
/** 毫秒时间戳 */
UNIX,
/** 日期 */
DATE,
/** 日期时间 */
DATE_TIME
}
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.bean.Page;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 在 {@link com.imyeyu.spring.mapper.BaseMapper#selectPageResult(Page)} 方法忽略查询该属性
* <br />
* {@link com.imyeyu.spring.service.AbstractEntityService#page(Page)} 同上
*
* @author 夜雨
* @since 2025-12-12 14:54
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface PageIgnore {
}

View File

@ -17,5 +17,10 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface Table {
/**
* 指定表名
*
* @return 表名
*/
String value();
}

View File

@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotBlank;
/**
* 含验证码数据实体
*
* @param <T> 数据体类型
* @author 夜雨
* @version 2021-03-01 17:10
*/
@ -21,26 +22,62 @@ public class CaptchaData<T> {
/** 数据体 */
protected T data;
/**
* 创建验证码数据实体
*/
public CaptchaData() {
}
/**
* 获取验证码 ID
*
* @return 验证码 ID
*/
public String getCaptchaId() {
return captchaId;
}
/**
* 设置验证码 ID
*
* @param captchaId 验证码 ID
*/
public void setCaptchaId(String captchaId) {
this.captchaId = captchaId;
}
/**
* 获取验证码
*
* @return 验证码
*/
public String getCaptcha() {
return captcha;
}
/**
* 设置验证码
*
* @param captcha 验证码
*/
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
/**
* 获取数据体
*
* @return 数据体
*/
public T getData() {
return data;
}
/**
* 设置数据体
*
* @param data 数据体
*/
public void setData(T data) {
this.data = data;
}

View File

@ -1,29 +1,41 @@
package com.imyeyu.spring.bean;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.spring.entity.Updatable;
/**
* 多语言实体基类
*
* @author 夜雨
* @since 2025-10-17 15:21
*/
public class Multilingual extends UUIDEntity {
public class Multilingual extends Language implements IDEntity<String>, Creatable, Updatable, Deletable {
protected String key;
/** 唯一标识 */
@Id
@AutoUUID
protected String id;
protected String zhCN;
/** 创建时间 */
protected Long createdAt;
protected String zhTW;
/** 更新时间 */
protected Long updatedAt;
protected String enUS;
/** 删除时间 */
protected Long deletedAt;
protected String ruRU;
protected String koKR;
protected String jaJP;
protected String deDE;
/**
* 创建多语言实体
*/
public Multilingual() {
}
/**
* 获取指定语言值
@ -31,7 +43,7 @@ public class Multilingual extends UUIDEntity {
* @param language 指定语言
* @return 值
*/
public String getValue(com.imyeyu.java.bean.Language language) {
public String getValue(Language.Enum language) {
try {
return Ref.getFieldValue(this, language.toString().replace("_", ""), String.class);
} catch (IllegalAccessException e) {
@ -39,67 +51,43 @@ public class Multilingual extends UUIDEntity {
}
}
public String getKey() {
return key;
@Override
public String getId() {
return id;
}
public void setKey(String key) {
this.key = key;
@Override
public void setId(String id) {
this.id = id;
}
public String getZhCN() {
return zhCN;
@Override
public Long getCreatedAt() {
return createdAt;
}
public void setZhCN(String zhCN) {
this.zhCN = zhCN;
@Override
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public String getZhTW() {
return zhTW;
@Override
public Long getUpdatedAt() {
return updatedAt;
}
public void setZhTW(String zhTW) {
this.zhTW = zhTW;
@Override
public void setUpdatedAt(Long updatedAt) {
this.updatedAt = updatedAt;
}
public String getEnUS() {
return enUS;
@Override
public Long getDeletedAt() {
return deletedAt;
}
public void setEnUS(String enUS) {
this.enUS = enUS;
}
public String getRuRU() {
return ruRU;
}
public void setRuRU(String ruRU) {
this.ruRU = ruRU;
}
public String getKoKR() {
return koKR;
}
public void setKoKR(String koKR) {
this.koKR = koKR;
}
public String getJaJP() {
return jaJP;
}
public void setJaJP(String jaJP) {
this.jaJP = jaJP;
}
public String getDeDE() {
return deDE;
}
public void setDeDE(String deDE) {
this.deDE = deDE;
@Override
public void setDeletedAt(Long deletedAt) {
this.deletedAt = deletedAt;
}
}

View File

@ -10,44 +10,117 @@ import java.util.LinkedHashMap;
/**
* 抽象页面查询参数
*
* @param <T> 查询示例类型
* @author 夜雨
* @version 2023-06-02 14:47
*/
public class Page extends BasePage {
public class Page<T> extends BasePage {
/** 精确匹配示例 */
protected T equalsExample;
/** 模糊匹配示例 */
protected T likesExample;
/** 排序字段映射 */
protected LinkedHashMap<String, BaseMapper.OrderType> orderMap;
/**
* 创建分页参数
*/
public Page() {
}
/**
* 创建分页参数
*
* @param index 页码
* @param size 每页数量
*/
public Page(int index, int size) {
super(index, size);
}
/**
* 获取偏移量
*
* @return 偏移量
*/
public long getOffset() {
return (long) index * size;
}
public int getLimit() {
/**
* 获取限制数量
*
* @return 限制数量
*/
public long getLimit() {
return size;
}
/**
* 获取精确匹配示例
*
* @return 精确匹配示例
*/
public T getEqualsExample() {
return equalsExample;
}
/**
* 设置精确匹配示例
*
* @param equalsExample 精确匹配示例
*/
public void setEqualsExample(T equalsExample) {
this.equalsExample = equalsExample;
}
/**
* 获取模糊匹配示例
*
* @return 模糊匹配示例
*/
public T getLikesExample() {
return likesExample;
}
/**
* 设置模糊匹配示例
*
* @param likesExample 模糊匹配示例
*/
public void setLikesExample(T likesExample) {
this.likesExample = likesExample;
}
/**
* 获取排序映射
*
* @return 排序映射
*/
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() {
return orderMap;
}
/**
* 设置排序映射
*
* @param orderMap 排序映射
*/
public void setOrderMap(LinkedHashMap<String, BaseMapper.OrderType> orderMap) {
this.orderMap = orderMap;
}
/**
* 添加排序字段
*
* @param field 字段名
* @param orderType 排序方式
*/
public void addOrder(String field, BaseMapper.OrderType orderType) {
orderMap = TimiJava.firstNotNull(orderMap, new LinkedHashMap<>());
orderMap = TimiJava.defaultIfNull(orderMap, new LinkedHashMap<>());
orderMap.put(Text.camelCase2underscore(field), orderType);
}
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
result.setList(pageMapper.listOrder(page.getOffset(), page.getLimit(), page.getOrderMap()));
result.setTotal(pageMapper.count());
return result;
}
}

View File

@ -5,8 +5,15 @@ import com.imyeyu.java.bean.BasePageResult;
/**
* 抽象页面查询结果
*
* @param <T> 列表元素类型
* @author 夜雨
* @version 2023-06-02 14:47
*/
public class PageResult<T> extends BasePageResult<T> {
/**
* 创建分页结果
*/
public PageResult() {
}
}

View File

@ -8,6 +8,12 @@ package com.imyeyu.spring.bean;
*/
public class RedisConfigParams {
/**
* 创建 Redis 配置参数
*/
public RedisConfigParams() {
}
/** 地址 */
private String host;

View File

@ -1,39 +1,72 @@
package com.imyeyu.spring.bean;
/**
* 请求范围参数
*
* @author 夜雨
* @since 2025-07-14 17:09
*/
public class RequestRange {
/** 起始值 */
private long start;
/** 结束值 */
private long end;
private long length;
/**
* 创建请求范围
*
* @param start 起始值
* @param end 结束值
*/
public RequestRange(long start, long end) {
this.start = start;
this.end = end;
}
/**
* 获取起始值
*
* @return 起始值
*/
public long getStart() {
return start;
}
/**
* 设置起始值
*
* @param start 起始值
*/
public void setStart(long start) {
this.start = start;
}
/**
* 获取结束值
*
* @return 结束值
*/
public long getEnd() {
return end;
}
/**
* 设置结束值
*
* @param end 结束值
*/
public void setEnd(long end) {
this.end = end;
}
/**
* 获取范围长度
*
* @return 范围长度
*/
public long getLength() {
return end - start + 1;
}
}
}

View File

@ -21,6 +21,12 @@ import java.time.Duration;
* @version 2021-11-21 10:00
*/
public abstract class AbstractRedisConfig implements CachingConfigurer {
/**
* 创建 Redis 配置
*/
protected AbstractRedisConfig() {
}
/**
* 构建 Redis 基本配置

View File

@ -1,5 +1,6 @@
package com.imyeyu.spring.entity;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.utils.Time;
import java.io.Serializable;
@ -12,6 +13,12 @@ import java.io.Serializable;
*/
public class BaseEntity implements Serializable, Creatable, Updatable, Deletable {
/**
* 创建基础实体
*/
public BaseEntity() {
}
/** 创建时间 */
protected Long createdAt;
@ -19,6 +26,7 @@ public class BaseEntity implements Serializable, Creatable, Updatable, Deletable
protected Long updatedAt;
/** 删除时间 */
@DeleteColumn
protected Long deletedAt;
/**

View File

@ -1,5 +1,7 @@
package com.imyeyu.spring.entity;
import com.imyeyu.utils.Time;
/**
* 可软删除实体
*
@ -27,5 +29,7 @@ public interface Deletable {
*
* @return true 为已删除
*/
boolean isDeleted();
default boolean isDeleted() {
return getDeletedAt() != null && getDeletedAt() < Time.now();
}
}

View File

@ -14,6 +14,12 @@ public class Entity extends BaseEntity implements IDEntity<Long> {
@Id
protected Long id;
/**
* 创建基础 ID 实体
*/
public Entity() {
}
/**
* 获取 ID
*

View File

@ -3,6 +3,7 @@ package com.imyeyu.spring.entity;
/**
* ID 实体
*
* @param <T> ID 类型
* @author 夜雨
* @since 2025-02-07 17:10
*/

View File

@ -16,6 +16,12 @@ public class UUIDEntity extends BaseEntity implements IDEntity<String> {
@AutoUUID
protected String id;
/**
* 创建 UUID 实体
*/
public UUIDEntity() {
}
public String getId() {
return id;
}

View File

@ -19,6 +19,12 @@ import java.sql.SQLException;
*/
public class GsonHandler extends BaseTypeHandler<JsonElement> {
/**
* 创建 Gson 类型处理器
*/
public GsonHandler() {
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JsonElement parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.toString());

View File

@ -1,5 +1,7 @@
package com.imyeyu.spring.mapper;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.SQLProvider;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
@ -8,11 +10,12 @@ import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import java.util.List;
import java.util.Map;
/**
* 基本 SQL 映射,子接口可以不实现
* 基本 SQL 映射
*
* @param <T> 实体类型
* @param <P> 主键类型
* @author 夜雨
* @version 2021-07-16 09:40
*/
@ -26,46 +29,66 @@ public interface BaseMapper<T, P> {
*/
enum OrderType {
/** 升序 */
ASC,
/** 降序 */
DESC
}
static final String NOT_DELETE = " AND `deleted_at` IS NULL ";
/** 当前时间戳毫秒 */
String UNIX_TIME = " FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) ";
static final String LIMIT_1 = " LIMIT 1";
/** 未删除条件 */
String NOT_DELETE = " AND (`deleted_at` IS NULL OR " + UNIX_TIME + " < `deleted_at`) ";
static final String UNIX_TIME = " FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) ";
/** 限制一条 */
String LIMIT_1 = " LIMIT 1";
static final String PAGE = NOT_DELETE + " LIMIT #{offset}, #{limit}";
/** 分页限制 */
String PAGE = NOT_DELETE + " LIMIT #{offset}, #{limit}";
/**
* 统计数据量
* 根据 Page 对象查询数据列表
*
* @param page 分页参数
* @return 数据列表
*/
@SelectProvider(type = SQLProvider.class, method = "selectByPage")
List<T> selectByPage(Page<T> page);
/**
* 根据 Page 对象统计数据量
*
* @param page 分页参数
* @return 数据量
*/
@SelectProvider(type = SQLProvider.class, method = "count")
long count();
@SelectProvider(type = SQLProvider.class, method = "countByPage")
long countByPage(Page<T> page);
default List<T> list(long offset, int limit) {
return listOrder(offset, limit, null);
/**
* 分页查询
*
* @param page 分页参数
* @return 分页结果
*/
default PageResult<T> selectPageResult(Page<T> page) {
PageResult<T> result = new PageResult<>();
result.setTotal(countByPage(page));
result.setList(selectByPage(page));
return result;
}
/**
* 获取部分数据
* 查询全部数据
*
* @param offset 偏移
* @param limit 数据量
* @return 数据列表
*/
@SelectProvider(type = SQLProvider.class, method = "listOrder")
List<T> listOrder(long offset, int limit, Map<String, OrderType> orderMap);
@SelectProvider(type = SQLProvider.class, method = "listAll")
List<T> listAll();
@SelectProvider(type = SQLProvider.class, method = "selectAll")
List<T> selectAll();
/**
* 创建数据。默认自增主键为 id如需修改请重写此接口
* 创建数据
*
* @param t 数据对象
*/
@ -82,9 +105,21 @@ public interface BaseMapper<T, P> {
@SelectProvider(type = SQLProvider.class, method = "select")
T select(P id);
/**
* 根据示例查询单条数据
*
* @param t 示例对象
* @return 数据对象
*/
@SelectProvider(type = SQLProvider.class, method = "selectByExample")
T selectByExample(T t);
/**
* 根据示例查询全部数据
*
* @param t 示例对象
* @return 数据列表
*/
@SelectProvider(type = SQLProvider.class, method = "selectAllByExample")
List<T> selectAllByExample(T t);
@ -96,6 +131,11 @@ public interface BaseMapper<T, P> {
@UpdateProvider(type = SQLProvider.class, method = "update")
void update(T t);
/**
* 选择性更新
*
* @param t 数据对象
*/
@UpdateProvider(type = SQLProvider.class, method = "updateSelective")
void updateSelective(T t);
@ -107,6 +147,14 @@ public interface BaseMapper<T, P> {
@UpdateProvider(type = SQLProvider.class, method = "delete")
void delete(P id);
/**
* 根据示例批量逻辑删除
*
* @param t 示例对象
*/
@UpdateProvider(type = SQLProvider.class, method = "deleteAllByExample")
void deleteAllByExample(T t);
/**
* 销毁(物理删除)
*

View File

@ -0,0 +1,52 @@
package com.imyeyu.spring.mapper;
import com.imyeyu.spring.util.RawSQLProvider;
import org.apache.ibatis.annotations.SelectProvider;
import java.util.List;
/**
* 原始 SQL 映射
*
* @param <T> 实体类型
* @param <P> 主键类型
* @author 夜雨
* @since 2026-01-05 12:58
*/
public interface RawMapper<T, P> {
/**
* 查询全部数据
*
* @return 数据列表
*/
@SelectProvider(type = RawSQLProvider.class, method = "selectAll")
List<T> selectAllRaw();
/**
* 根据 ID 获取对象
*
* @param id 索引
* @return 数据对象
*/
@SelectProvider(type = RawSQLProvider.class, method = "select")
T selectRaw(P id);
/**
* 根据示例查询单条数据
*
* @param t 示例对象
* @return 数据对象
*/
@SelectProvider(type = RawSQLProvider.class, method = "selectByExample")
T selectByExampleRaw(T t);
/**
* 根据示例查询全部数据
*
* @param t 示例对象
* @return 数据列表
*/
@SelectProvider(type = RawSQLProvider.class, method = "selectAllByExample")
List<T> selectAllByExampleRaw(T t);
}

View File

@ -3,6 +3,8 @@ package com.imyeyu.spring.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.Updatable;
import com.imyeyu.spring.mapper.BaseMapper;
/**
@ -18,7 +20,17 @@ public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
/** 基本 Mapper */
protected BaseMapper<T, P> baseMapper;
/** @return Mapper 实例 */
/**
* 创建实体服务
*/
protected AbstractEntityService() {
}
/**
* 获取 Mapper 实例
*
* @return Mapper 实例
*/
protected abstract BaseMapper<T, P> mapper();
/** 检查 mapper */
@ -30,13 +42,19 @@ public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
}
@Override
public PageResult<T> page(Page page) {
public PageResult<T> page(Page<T> page) {
checkMapper();
return Page.toResult(baseMapper, page, new PageResult<>());
return baseMapper.selectPageResult(page);
}
public void create(T t) {
checkMapper();
if (t instanceof Updatable updatable) {
updatable.setUpdatedAt(null);
}
if (t instanceof Deletable deletable) {
deletable.setDeletedAt(null);
}
baseMapper.insert(t);
}

View File

@ -5,6 +5,7 @@ import com.imyeyu.java.bean.timi.TimiException;
/**
* 可软删除实体服务
*
* @param <P> 主键类型
* @author 夜雨
* @since 2025-05-14 17:30
*/

View File

@ -5,6 +5,7 @@ import com.imyeyu.java.bean.timi.TimiException;
/**
* 可销毁(物理删除)实体服务
*
* @param <P> 主键类型
* @author 夜雨
* @since 2025-05-14 17:30
*/

View File

@ -18,5 +18,5 @@ public interface PageableService<T> {
* @param page 页面查询参数
* @return 查询页面结果
*/
PageResult<T> page(Page page);
PageResult<T> page(Page<T> page);
}

View File

@ -9,11 +9,19 @@ import java.lang.annotation.Annotation;
/**
* 数据验证动态消息返回抽象类
*
* @param <A> 注解类型
* @param <T> 校验数据类型
* @author 夜雨
* @version 2023-05-07 00:08
*/
public abstract class AbstractValidator<A extends Annotation, T> implements ConstraintValidator<A, T> {
/**
* 创建校验器
*/
protected AbstractValidator() {
}
/**
* 验证处理器,入参验证数据,返回错误消息语言映射,返回 null 时表示通过验证
*
@ -32,4 +40,4 @@ public abstract class AbstractValidator<A extends Annotation, T> implements Cons
}
return true;
}
}
}

View File

@ -28,12 +28,20 @@ public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final String DEV_LANG_CONFIG = "dev.lang";
/**
* 创建全局异常处理器
*/
public GlobalExceptionHandler() {
}
@Value("${spring.profiles.active}")
private String env;
/**
* @param e
* @return
* 消息转换异常
*
* @param e 异常
* @return 异常返回
*/
@ExceptionHandler(HttpMessageConversionException.class)
public TimiResponse<?> conversionException(HttpMessageConversionException e) {

View File

@ -32,8 +32,15 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
/** 多语言头处理回调 */
private CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader;
/**
* 创建全局返回处理器
*/
public GlobalReturnHandler() {
}
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return Objects.requireNonNull(returnType.getMethod()).getAnnotation(IgnoreGlobalReturn.class) == null;
@ -71,10 +78,20 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
return result;
}
/**
* 获取多语言头处理回调
*
* @return 处理回调
*/
public CallbackArgReturn<LanguageMsgMapping<?>, String> getMultilingualHeader() {
return multilingualHeader;
}
/**
* 设置多语言头处理回调
*
* @param multilingualHeader 处理回调
*/
public void setMultilingualHeader(CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader) {
this.multilingualHeader = multilingualHeader;
}

View File

@ -0,0 +1,49 @@
package com.imyeyu.spring.util;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.builder.annotation.ProviderContext;
import java.util.stream.Collectors;
/**
* 原始 Mapper SQL 代理器
*
* @author 夜雨
* @since 2026-01-05 13:00
*/
public class RawSQLProvider extends SQLProvider {
@Override
public String selectAll(ProviderContext context) {
EntityMeta meta = getEntityMeta(context);
return "SELECT * FROM %s WHERE 1 = 1".formatted(meta.table);
}
@Override
public String select(ProviderContext context, Object id) {
EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
return "SELECT %s FROM `%s` WHERE `%s` = #{%s}".formatted(meta.selectAllClause, meta.table, meta.idFieldColumn.columnName, id) + " LIMIT 1";
}
@Override
public String selectByExample(Object entity) {
return selectAllByExample(entity) + BaseMapper.LIMIT_1;
}
@Override
public String selectAllByExample(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
String conditionClause = meta.fieldColumnList.stream()
.filter(fc -> fc.isNotEmpty(entity))
.map(fc -> "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName))
.collect(Collectors.joining(" AND "));
return "SELECT %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, meta.table, conditionClause);
}
@Override
public String deleteAllByExample(Object entity) {
return super.deleteAllByExample(entity);
}
}

View File

@ -22,6 +22,8 @@ import java.util.function.Consumer;
* RedisTemplate 功能封装,简化 Redis 操作
* <p>serializer 为该 RedisTemplate 的键的序列化操作,序列化解析器由 {@link AbstractRedisConfig} 提供
*
* @param <K> 键类型
* @param <V> 值类型
* @author 夜雨
* @version 2021-11-21 09:58
*/
@ -30,6 +32,12 @@ public class Redis<K, V> {
private final RedisSerializer<K> serializer;
private final RedisTemplate<K, V> redis;
/**
* 创建 Redis 操作封装
*
* @param redis RedisTemplate 实例
* @param serializer 键序列化器
*/
public Redis(RedisTemplate<K, V> redis, RedisSerializer<K> serializer) {
this.redis = redis;
this.serializer = serializer;
@ -47,9 +55,9 @@ public class Redis<K, V> {
/**
* 加锁
*
* @param key
* @param value
* @param timeoutMS
* @param key
* @param value
* @param timeoutMS 超时时间毫秒
* @return true 为加锁成功
*/
public boolean lock(K key, V value, long timeoutMS) {
@ -57,6 +65,11 @@ public class Redis<K, V> {
return lock != null && lock;
}
/**
* 释放锁
*
* @param key 键
*/
public void releaseLock(K key) {
destroy(key);
}

View File

@ -8,11 +8,17 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.nio.charset.StandardCharsets;
/**
* Redis 序列化工具
*
* @author 夜雨
* @version 2023-07-17 16:20
*/
public class RedisSerializers {
/** 工具类禁止实例化 */
private RedisSerializers() {
}
/** 字符串序列化 */
public static final StringRedisSerializer STRING = new StringRedisSerializer();
@ -76,7 +82,13 @@ public class RedisSerializers {
}
};
/** Gson 序列化 */
/**
* Gson 序列化
*
* @param <T> 数据类型
* @param clazz 数据类型
* @return Redis 序列化器
*/
public static <T> RedisSerializer<T> gsonSerializer(Class<T> clazz) {
return new RedisSerializer<>() {

View File

@ -1,15 +1,18 @@
package com.imyeyu.spring.util;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.BaseEntity;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.Destroyable;
@ -24,6 +27,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -41,42 +45,122 @@ public class SQLProvider {
/** 反射缓存 */
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
public String count(ProviderContext context) {
/**
* 创建 SQL 提供器
*/
public SQLProvider() {
}
/**
* 根据 Page 对象查询数据列表
*
* @param context 代理器上下文
* @param page 分页参数
* @return SQL
*/
public String selectByPage(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT %s FROM `%s` WHERE 1 = 1".formatted(meta.selectPageClause, meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
if (TimiJava.isNotEmpty(page.getEqualsExample())) {
// 精准查询
Object obj = page.getEqualsExample();
EntityMeta metaExample = getEntityMeta(obj.getClass());
String conditionClause = metaExample.fieldColumnList.stream()
.filter(fc -> fc.isNotEmpty(obj))
.map(fc -> "`%s` = '%s'".formatted(fc.columnName, fc.getAsString(obj)))
.collect(Collectors.joining(" AND "));
if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND ").append(conditionClause);
}
}
if (TimiJava.isNotEmpty(page.getLikesExample())) {
// 模糊查询
Object obj = page.getLikesExample();
EntityMeta metaExample = getEntityMeta(obj.getClass());
String conditionClause = metaExample.fieldColumnList.stream()
.filter(fc -> fc.isNotEmpty(obj))
.map(fc -> "`%s` LIKE CONCAT('%%', '%s', '%%')".formatted(fc.columnName, fc.getAsString(obj)))
.collect(Collectors.joining(" OR "));
if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND (").append(conditionClause).append(')');
}
}
// 排序
if (TimiJava.isNotEmpty(page.getOrderMap())) {
sql.append(" ORDER BY ");
for (Map.Entry<String, BaseMapper.OrderType> item : page.getOrderMap().entrySet()) {
sql.append("`%s` %s, ".formatted(
Text.camelCase2underscore(item.getKey()),
item.getValue().toString()
));
}
sql.deleteCharAt(sql.length() - 2);
} else {
// 默认排序
if (meta.canCreate && !meta.canUpdate) {
sql.append(" ORDER BY `created_at` DESC");
}
if (meta.canCreate && meta.canUpdate) {
sql.append(" ORDER BY COALESCE(`updated_at`, `created_at`) DESC");
}
}
// 分页
sql.append(" LIMIT #{offset}, #{limit}");
return sql.toString();
}
/**
* 根据 Page 对象统计数据量
*
* @param context 代理器上下文
* @param page 分页参数
* @return SQL
*/
public String countByPage(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT COUNT(*) FROM `%s` WHERE 1 = 1".formatted(meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
if (TimiJava.isNotEmpty(page.getEqualsExample())) {
// 精准查询
Object obj = page.getEqualsExample();
EntityMeta metaExample = getEntityMeta(obj.getClass());
String conditionClause = metaExample.fieldColumnList.stream()
.filter(fc -> fc.isNotEmpty(obj))
.map(fc -> "`%s` = '%s'".formatted(fc.columnName, fc.getAsString(obj)))
.collect(Collectors.joining(" AND "));
if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND ").append(conditionClause);
}
}
if (TimiJava.isNotEmpty(page.getLikesExample())) {
// 模糊查询
Object obj = page.getLikesExample();
EntityMeta metaExample = getEntityMeta(obj.getClass());
String conditionClause = metaExample.fieldColumnList.stream()
.filter(fc -> fc.isNotEmpty(obj))
.map(fc -> "`%s` LIKE CONCAT('%%', '%s', '%%')".formatted(fc.columnName, fc.getAsString(obj)))
.collect(Collectors.joining(" OR "));
if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND (").append(conditionClause).append(')');
}
}
return sql.toString();
}
public String listOrder(ProviderContext context, @Param("offset") Long offset, @Param("limit") Integer limit, @Param("orderMap") Map<String, BaseMapper.OrderType> orderMap) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT %s FROM `%s` WHERE 1 = 1".formatted(meta.selectAllClause, meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
if (TimiJava.isNotEmpty(orderMap)) {
sql.append(" ORDER BY ");
for (Map.Entry<String, BaseMapper.OrderType> item : orderMap.entrySet()) {
sql.append(Text.camelCase2underscore(item.getKey())).append(' ').append(item.getValue().toString());
sql.append(", ");
}
sql.deleteCharAt(sql.length() - 2);
} else {
if (meta.canCreate && !meta.canUpdate) {
sql.append(" ORDER BY created_at DESC");
}
if (meta.canCreate && meta.canUpdate) {
sql.append(" ORDER BY COALESCE(updated_at, created_at) DESC");
}
}
return sql.append(" LIMIT %s, %s".formatted(offset, limit)).toString();
}
public String listAll(ProviderContext context) {
/**
* 查询全部数据
*
* @param context 代理器上下文
* @return SQL
*/
public String selectAll(ProviderContext context) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM %s WHERE 1 = 1".formatted(meta.table));
@ -90,6 +174,7 @@ public class SQLProvider {
* 插入
* <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
*
* @param context 代理器上下文
* @param entity 实体
* @return SQL
*/
@ -154,16 +239,8 @@ public class SQLProvider {
public String selectAllByExample(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
String conditionClause = meta.fieldColumnList.stream()
.filter(fc -> {
try {
return Ref.getFieldValue(entity, fc.field, Object.class) != null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.map(fc -> {
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
})
.filter(fc -> fc.isNotEmpty(entity))
.map(fc -> "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName))
.collect(Collectors.joining(" AND "));
StringBuilder sql = new StringBuilder();
@ -188,14 +265,12 @@ public class SQLProvider {
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
TimiException.required(meta.canUpdate, "not allow update for %s".formatted(meta.entityClass));
if (entity instanceof Updatable updatable) {
updatable.setUpdatedAt(Time.now());
}
String setClause = meta.fieldColumnList.stream()
.filter(fc -> !fc.isId)
.map(fc -> {
if (entity instanceof Updatable updatableEntity) {
updatableEntity.setUpdatedAt(Time.now());
}
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
})
.filter(FieldColumn::isNotId)
.map(fc -> "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName))
.collect(Collectors.joining(", "));
return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
}
@ -211,27 +286,13 @@ public class SQLProvider {
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
TimiException.required(meta.canUpdate, "not allow update for %s".formatted(meta.entityClass));
if (entity instanceof BaseEntity baseEntity) {
baseEntity.setCreatedAt(null);
baseEntity.setDeletedAt(null);
if (entity instanceof Updatable updatable) {
updatable.setUpdatedAt(Time.now());
}
String setClause = meta.fieldColumnList.stream()
.filter(fc -> {
if (fc.isId) {
return false;
}
try {
return Ref.getFieldValue(entity, fc.field, Object.class) != null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.map(fc -> {
if (entity instanceof Updatable updatableEntity) {
updatableEntity.setUpdatedAt(Time.now());
}
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
})
.filter(FieldColumn::isNotId)
.filter(fc -> fc.isNotNull(entity))
.map(fc -> "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName))
.collect(Collectors.joining(", "));
return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
}
@ -251,6 +312,35 @@ public class SQLProvider {
return "UPDATE `%s` SET `deleted_at` = %s WHERE `%s` = #{id}".formatted(meta.table, Time.now(), meta.idFieldColumn.columnName);
}
/**
* 根据示例批量逻辑删除
*
* @param entity 实体
* @return SQL
*/
public String deleteAllByExample(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
TimiException.required(meta.canDelete, "not allow delete for %s".formatted(meta.entityClass));
FieldColumn deleteColumn = meta.getFieldColumnList().stream().filter(fc -> fc.isDeleteColumn).findFirst().orElse(null);
TimiException.required(deleteColumn, "unknown delete column, use com.imyeyu.spring.annotation.table.DeleteColumn annotation on field");
assert deleteColumn != null;
assert deleteColumn.deleteColumnType != null;
String delClause = meta.fieldColumnList.stream()
.filter(FieldColumn::isNotId)
.filter(fc -> fc.isNotEmpty(entity))
.map(fc -> "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName))
.collect(Collectors.joining(" AND "));
StringBuilder sql = new StringBuilder("UPDATE `%s` SET `%s` = ".formatted(meta.table, deleteColumn.getColumnName()));
sql.append("'").append(switch (deleteColumn.deleteColumnType) {
case UNIX -> Time.now();
case DATE, DATE_TIME -> new Date();
}).append("'");
sql.append(" WHERE ").append(delClause);
return sql.toString();
}
/**
* 硬删除,需要实体实现 {@link Destroyable}
*
@ -306,6 +396,9 @@ public class SQLProvider {
/** 查询字段映射 */
final String selectAllClause;
/** 页面查询字段映射 */
final String selectPageClause;
/** ID 字段 */
final FieldColumn idFieldColumn;
@ -324,6 +417,11 @@ public class SQLProvider {
/** true 为可销毁(硬删除) */
final boolean canDestroy;
/**
* 创建实体元数据
*
* @param entityClass 实体类型
*/
public EntityMeta(Class<?> entityClass) {
this.entityClass = entityClass;
@ -339,8 +437,6 @@ public class SQLProvider {
TimiException.required(this.table, String.format("empty table annotation value for %s entity", entityClass.getName()));
}
List<Field> allFieldList = Ref.listAllFields(entityClass);
StringBuilder selectAllClause = new StringBuilder();
FieldColumn idFieldColumn = null;
List<FieldColumn> fieldColumnList = new ArrayList<>();
for (int i = 0; i < allFieldList.size(); i++) {
@ -353,21 +449,10 @@ public class SQLProvider {
TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName()));
idFieldColumn = fieldColumn;
}
{
Column column = field.getAnnotation(Column.class);
if (column == null) {
selectAllClause.append('`').append(fieldColumn.columnName).append('`');
selectAllClause.append(',');
} else {
// 处理自定义映射列名
selectAllClause.append('`').append(column.value()).append('`');
selectAllClause.append(" AS `").append(fieldColumn.fieldName).append('`');
selectAllClause.append(',');
}
}
fieldColumnList.add(fieldColumn);
}
this.selectAllClause = selectAllClause.substring(0, selectAllClause.length() - 1);
this.selectAllClause = buildSelectClause(fieldColumnList, null);
this.selectPageClause = buildSelectClause(fieldColumnList, fc -> !fc.getField().isAnnotationPresent(PageIgnore.class));
this.idFieldColumn = idFieldColumn;
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
canCreate = Creatable.class.isAssignableFrom(entityClass);
@ -376,38 +461,105 @@ public class SQLProvider {
canDestroy = Destroyable.class.isAssignableFrom(entityClass);
}
private String buildSelectClause(List<FieldColumn> fieldColumnList, CallbackArgReturn<FieldColumn, Boolean> callback) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fieldColumnList.size(); i++) {
FieldColumn fieldColumn = fieldColumnList.get(i);
Field field = fieldColumn.getField();
if (callback != null && !callback.handler(fieldColumn)) {
continue;
}
Column column = field.getAnnotation(Column.class);
if (column == null) {
sb.append('`').append(fieldColumn.columnName).append('`');
sb.append(',');
} else {
// 自定义映射列名
sb.append('`').append(column.value()).append('`');
sb.append(" AS `").append(fieldColumn.fieldName).append('`');
sb.append(',');
}
}
return sb.substring(0, sb.length() - 1);
}
/**
* 获取实体类型
*
* @return 实体类型
*/
public Class<?> getEntityClass() {
return entityClass;
}
/**
* 获取表名
*
* @return 表名
*/
public String getTable() {
return table;
}
/**
* 获取查询字段映射
*
* @return 查询字段映射
*/
public String getSelectAllClause() {
return selectAllClause;
}
/**
* 获取 ID 字段映射
*
* @return ID 字段映射
*/
public FieldColumn getIdFieldColumn() {
return idFieldColumn;
}
/**
* 获取字段映射列表
*
* @return 字段映射列表
*/
public List<FieldColumn> getFieldColumnList() {
return fieldColumnList;
}
/**
* 是否可创建
*
* @return true 为可创建
*/
public boolean canCreate() {
return canCreate;
}
/**
* 是否可更新
*
* @return true 为可更新
*/
public boolean canUpdate() {
return canUpdate;
}
/**
* 是否可删除
*
* @return true 为可删除
*/
public boolean canDelete() {
return canDelete;
}
/**
* 是否可销毁
*
* @return true 为可销毁
*/
public boolean canDestroy() {
return canDestroy;
}
@ -438,6 +590,15 @@ public class SQLProvider {
final boolean isAutoUpperUUID;
final boolean isDeleteColumn;
final DeleteColumn.Type deleteColumnType;
/**
* 创建字段映射
*
* @param field 字段
*/
public FieldColumn(Field field) {
this.field = field;
@ -456,28 +617,135 @@ public class SQLProvider {
} else {
isAutoUpperUUID = false;
}
isDeleteColumn = field.isAnnotationPresent(DeleteColumn.class);
if (isDeleteColumn) {
deleteColumnType = field.getAnnotation(DeleteColumn.class).value();
} else {
deleteColumnType = null;
}
}
/**
* 判断字段值是否为空
*
* @param entity 实体
* @return true 为 null
*/
public boolean isNull(Object entity) {
try {
return Ref.getFieldValue(entity, field, Object.class) == null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 判断字段值是否非空
*
* @param entity 实体
* @return true 为非 null
*/
public boolean isNotNull(Object entity) {
return !isNull(entity);
}
/**
* 判断字段值是否为空
*
* @param entity 实体
* @return true 为空
*/
public boolean isEmpty(Object entity) {
try {
return TimiJava.isEmpty(Ref.getFieldValue(entity, field, Object.class));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 判断字段值是否非空
*
* @param entity 实体
* @return true 为非空
*/
public boolean isNotEmpty(Object entity) {
return !isEmpty(entity);
}
/**
* 获取字段字符串值
*
* @param obj 实体
* @return 字符串值
*/
public String getAsString(Object obj) {
try {
return field.get(obj).toString();
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 获取字段
*
* @return 字段
*/
public Field getField() {
return field;
}
/**
* 获取字段名
*
* @return 字段名
*/
public String getFieldName() {
return fieldName;
}
/**
* 获取列名
*
* @return 列名
*/
public String getColumnName() {
return columnName;
}
/**
* 是否为 ID 字段
*
* @return true 为 ID 字段
*/
public boolean isId() {
return isId;
}
/**
* 是否非 ID 字段
*
* @return true 为非 ID 字段
*/
public boolean isNotId() {
return !isId();
}
/**
* 是否自动 UUID
*
* @return true 为自动 UUID
*/
public boolean isAutoUUID() {
return isAutoUUID;
}
/**
* 是否自动大写 UUID
*
* @return true 为自动大写 UUID
*/
public boolean isAutoUpperUUID() {
return isAutoUpperUUID;
}

View File

@ -9,13 +9,19 @@ import java.io.IOException;
import java.util.List;
/**
*
* Yaml 属性源加载工厂
*
* @author 夜雨
* @since 2025-10-13 16:29
*/
public class YamlPropertySourceFactory implements PropertySourceFactory {
/**
* 创建 Yaml 属性源工厂
*/
public YamlPropertySourceFactory() {
}
@Override
public @org.springframework.lang.NonNull PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());