Initial project

This commit is contained in:
Timi
2025-07-08 14:34:32 +08:00
parent 271e2ae673
commit c27146aa91
56 changed files with 3050 additions and 80 deletions

89
.gitignore vendored
View File

@ -1,57 +1,39 @@
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
*.iml
# AWS User-specific
.idea/**/aws.xml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# Generated files
.idea/**/contentModel.xml
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
@ -59,23 +41,10 @@ out/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> Maven
target/
@ -87,12 +56,4 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath

18
LICENSE
View File

@ -1,18 +1,8 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2025 timi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +1,13 @@
# timi-spring
## timi-spring
Timi SpringBoot 通用依赖
_SpringBoot 基本封装_
### Maven 引用
### 文档
### 反馈
### 日志
### 协议

108
pom.xml Normal file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
<groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<properties>
<springboot.version>3.4.0</springboot.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.test.skip>true</maven.test.skip>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.imyeyu.io</groupId>
<artifactId>timi-io</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,360 @@
package com.imyeyu.spring;
import com.google.gson.Gson;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.bean.timi.TimiResponse;
import com.imyeyu.java.ref.Ref;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/**
* TimiSpring
*
* <p>如果使用本依赖相关组件,务必让 SpringBoot 扫描本依赖的包,在 SpringApplication 上加上注解
* <pre>
* &#64;ComponentScan({"自己扫描的包", "com.imyeyu.spring"})
* </pre>
*
* @author 夜雨
* @version 2021-11-20 17:16
*/
public class TimiSpring {
/** 版本号 */
public static final String VERSION = "0.0.1";
private static final Logger log = LoggerFactory.getLogger(TimiSpring.class);
private static final Gson GSON = new Gson();
/**
* 回调数据
*
* @param response 返回
* @param resp 返回结果
*/
public static void render(HttpServletResponse response, TimiResponse<?> resp) {
try {
HttpSession session = getSession();
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write(GSON.toJson(resp).getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (Exception e) {
log.error("TimiSpring.render Error", e);
}
}
/**
* 回调错误
*
* @param response 返回
* @param code 代码
* @param msgKey 消息映射键
*/
public static void renderError(HttpServletResponse response, TimiCode code, String msgKey) {
try {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write(GSON.toJson(code.toResponse().msg(msgKey)).getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (Exception e) {
log.error("TimiSpring.renderError error", e);
}
}
/**
* 获取 Servlet 请求属性
*
* @return Servlet 请求属性
* @throws TimiException 请求异常
*/
public static ServletRequestAttributes getServletRequestAttributes() {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes sra) {
return sra;
}
throw new TimiException(TimiCode.ERROR_NPE_VARIABLE);
}
/**
* 获取 HttpServlet 请求
*
* @return HttpServlet 请求
*/
public static HttpServletRequest getRequest() {
return getServletRequestAttributes().getRequest();
}
public static String getDomain() {
return getRequest().getServerName();
}
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);
}
public static String getURL() {
return getRequest().getRequestURL().toString();
}
public static String getURI() {
return getRequest().getRequestURI();
}
/**
* 获取 HttpServlet 回调
*
* @return HttpServlet 回调
*/
public static HttpServletResponse getResponse() {
return getServletRequestAttributes().getResponse();
}
/**
* 获取 Http 会话
*
* @return Http 会话
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
/**
* 获取请求地理区域
*
* @return 地区
*/
public static Locale getLocale() {
return getRequest().getLocale();
}
/**
* 获取请求头属性
*
* @param key 属性键
* @return 属性值
*/
public static String getHeader(String key) {
return getRequest().getHeader(key);
}
/**
* 获取会话数据
*
* @param key 键
* @return 值
*/
public static Object getSessionAttr(String key) {
return getSession().getAttribute(key);
}
/**
* 获取会话数据
*
* @param key 键
* @return 值
*/
public static boolean hasSessionAttr(String key) {
return getSessionAttr(key) != null;
}
/**
* 获取会话数据(字符串)
*
* @param key 键
* @return 值
*/
public static String getSessionAttrAsString(String key) {
Object sessionAttr = getSessionAttr(key);
if (sessionAttr == null) {
return null;
}
return sessionAttr.toString();
}
/**
* 获取会话数据
*
* @param key 键
* @param clazz 值类型
* @param <T> 值类型
* @return 值
*/
public static <T> T getSessionAttr(String key, Class<T> clazz) {
return clazz.cast(getSessionAttr(key));
}
/**
* 设置会话数据
*
* @param key 键
* @param t 值
* @param <T> 值类型
*/
public static <T> void setSessionAttr(String key, T t) {
getSession().setAttribute(key, t);
}
/**
* 移除会话数据
*
* @param key 键
*/
public static void removeSessionAttr(String key) {
getSession().removeAttribute(key);
}
/**
* 获取请求数据
*
* @param key 键
* @return 值
*/
public static Object getRequestAttr(String key) {
return getRequest().getAttribute(key);
}
/**
* 获取请求数据
*
* @param key 键
* @return 值
*/
public static boolean hasRequestAttr(String key) {
return getRequestAttr(key) != null;
}
/**
* 获取请求数据(字符串)
*
* @param key 键
* @return 值
*/
public static String getRequestAttrAsString(String key) {
Object reqAttr = getRequestAttr(key);
if (reqAttr == null) {
return null;
}
return reqAttr.toString();
}
/**
* 获取请求数据
*
* @param key 键
* @param clazz 值类型
* @param <T> 值类型
* @return 值
*/
public static <T> T getRequestAttr(String key, Class<T> clazz) {
return clazz.cast(getRequestAttr(key));
}
/**
* 设置请求数据
*
* @param key 键
* @param t 值
* @param <T> 值类型
*/
public static <T> void setRequestAttr(String key, T t) {
getRequest().setAttribute(key, t);
}
/**
* 移除请求数据
*
* @param key 键
*/
public static void removeRequestAttr(String key) {
getRequest().removeAttribute(key);
}
public static void addCookie(Cookie cookie) {
getResponse().addCookie(cookie);
}
public static void addCookie(String key, String value) {
addCookie(new Cookie(key, value));
}
public static Cookie getCookie(String key) {
Cookie[] cookies = getRequest().getCookies();
if (cookies == null) {
return null;
}
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals(key)) {
return cookies[i];
}
}
return null;
}
/**
* 获取请求头的令牌,键为 Token
*
* @return 令牌
*/
public static String getToken() {
return getHeader("Token");
}
/**
*
* @return 客户端地区语言
*/
public static Language getLanguage() {
String name = TimiSpring.getHeader("Language");
if (TimiJava.isEmpty(name)) {
name = TimiSpring.getLocale().toString();
}
if (TimiJava.isEmpty(name)) { // use for not support
return Language.zh_CN;
}
return Ref.toType(Language.class, name);
}
/**
* 获取请求 IP
*
* @return 请求 IP
* @throws TimiException 服务异常
*/
public static String getRequestIP() {
String ip = getHeader("x-forwarded-for");
// nginx 转发
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = getHeader("X-Forwarded-For");
}
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = getHeader("X-Real-IP");
}
// 默认获取
if (TimiJava.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = getRequest().getRemoteAddr();
}
// 本地 IP
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
public static boolean isLocalIP() {
return getRequestIP().startsWith("127");
}
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.spring.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* AOP 切面日志注解,应用在 Controller 的接口上
*
* @author 夜雨
* @version 2021-07-21 23:34
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AOPLog {
}

View File

@ -0,0 +1,110 @@
package com.imyeyu.spring.annotation;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.utils.Text;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* AOP 切面日志
*
* @author 夜雨
* @version 2021-08-17 16:26
*/
@Aspect
@Component
public class AOPLogInterceptor {
public static final String REQUEST_ID = "TIMI_SPRING_REQUEST_ID";
private static final Logger log = LoggerFactory.getLogger(AOPLogInterceptor.class);
/** 注入注解 */
@Pointcut("@annotation(annotation.com.imyeyu.spring.AOPLog)")
public void logPointCut() {
}
/**
* 执行前
*
* @param joinPoint 切入点
*/
@Before("logPointCut()")
public void doBefore(JoinPoint joinPoint) {
String uuid = Text.tempUUID();
TimiSpring.setSessionAttr(REQUEST_ID, uuid);
log.info("ID: {} Request -> IP: {}, URI: {}", uuid, TimiSpring.getRequestIP(), TimiSpring.getRequest().getRequestURI());
}
/**
* 执行后
*
* @param response 返回内容
* @throws Throwable 异常
*/
@AfterReturning(returning = "response", pointcut = "logPointCut()")
public void doAfterReturning(Object response) throws Throwable {
String msg = "ID: {} Response <- Return.";
if (response instanceof IDEntity<?> entity) {
// 返回实体
msg += entity.getClass().getSimpleName() + "." + entity.getId();
} else if (response instanceof PageResult<?> pageResult) {
// 返回数组
if (pageResult.getList().isEmpty()) {
msg += "PageResult<?> Empty";
} else {
if (pageResult.getList().get(0) == null) {
msg += "PageResult<?>." + pageResult.getList().size();
} else {
msg += "PageResult<" + pageResult.getList().get(0).getClass().getSimpleName() + ">[" + pageResult.getList().size() + "]";
}
}
// 返回数据页
} else if (response instanceof String string) {
// 返回字符串
if (string.length() < 64) {
msg += string;
} else {
msg += string.substring(0, 64) + "..";
}
msg = msg.replaceAll("[\\r\\n]+", "");
} else if (response instanceof Boolean bool) {
// 返回布尔值
msg += bool;
} else if (response instanceof Number number) {
// 返回数字
msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]";
} else {
// 其他对象
if (TimiJava.isNotEmpty(response)) {
msg += response.getClass().getSimpleName();
} else {
msg += "NULL";
}
}
log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID));
}
/**
* 环绕
*
* @param pjp 切入点
* @return 执行返回
* @throws Throwable 异常
*/
@Around("logPointCut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
}

View File

@ -0,0 +1,46 @@
package com.imyeyu.spring.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 实体注解Component 别名,只是为了在实体类注入服务接口(如果实体需要注入服务,需要这个类注解)
* <pre>
*
* &#64;Entity
* &#64;NoArgsConstructor // 需要个空的构造方法让 MyBatis 正常实例化
* public class Entity {
*
* &#64;Transient
* private transient static Service service;
*
* // 通过构造方法注入
* &#64;Autowired
* public Entity(Service service) {
* Entity.service = service;
* }
* }
*
* </pre>
*
* @author 夜雨
* @version 2021-08-18 16:31
*/
@Component
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
/**
* 设置控制反转 ID
*
* @return 控制反转 ID
*/
String value() default "";
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.spring.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 忽略全局返回处理器注解
*
* @author 夜雨
* @version 2023-07-16 10:58
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreGlobalReturn {
}

View File

@ -0,0 +1,36 @@
package com.imyeyu.spring.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 应用在 Controller 的接口上
*
* <p>{@link #lifeCycle()} 生命周期内(秒)限制访问 {@link #value()} 次
*
* @author 夜雨
* @version 2021-08-16 17:57
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RequestRateLimit {
/**
* 生命周期内访问限制,默认 10 秒内 10 次
*
* @return 生命周期内限制访问次数
*/
int value() default 10;
/**
* 生命周期
*
* @return 生命周期秒数
*/
int lifeCycle() default 10;
}

View File

@ -0,0 +1,56 @@
package com.imyeyu.spring.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 抽象访问频率限制,具体子类实现
*
* @author 夜雨
* @version 2021-08-16 18:07
*/
public abstract class RequestRateLimitAbstractInterceptor implements HandlerInterceptor {
protected String buildId(HandlerMethod handlerMethod) {
return handlerMethod.getMethod().getDeclaringClass().getSimpleName() + "." + handlerMethod.getMethod().getName();
}
/**
* 处理前
*
* @param req 请求
* @param resp 返回
* @param handler 处理方法
* @return true 为通过
*/
@Override
public boolean preHandle(@NonNull HttpServletRequest req,
@NonNull HttpServletResponse resp,
@NonNull Object handler) {
// 方法注解
if (handler instanceof HandlerMethod handlerMethod) {
RequestRateLimit requestRateLimit = handlerMethod.getMethodAnnotation(RequestRateLimit.class);
if (requestRateLimit == null) {
return true;
}
// 频率限制
return beforeRun(req, resp, buildId(handlerMethod), requestRateLimit.lifeCycle(), requestRateLimit.value());
}
return true;
}
/**
* 接口访问前触发
*
* @param req 请求
* @param resp 返回
* @param id 方法
* @param cycle 生命周期
* @param limit 访问限制
* @return true 为通过
*/
public abstract boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.spring.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 单字段 Json 数据体
*
* @author 夜雨
* @version 2023-08-09 10:36
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestSingleParam {
}

View File

@ -0,0 +1,53 @@
package com.imyeyu.spring.annotation;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class RequestSingleParamResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestSingleParam.class);
}
@Override
public Object resolveArgument(@Nonnull MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new TimiException(TimiCode.REQUEST_BAD, "request illegal");
}
JsonElement element = JsonParser.parseString(IO.toString(request.getInputStream()));
if (!element.isJsonObject()) {
throw new TimiException(TimiCode.ARG_BAD, "not json object");
}
JsonObject object = element.getAsJsonObject();
String parameterName = parameter.getParameterName();
if (!object.has(parameterName)) {
throw new TimiException(TimiCode.ARG_MISS, "not found " + parameterName + " param");
}
JsonElement el = object.get(parameterName);
if (parameter.getParameterType().isAssignableFrom(Long.class)) {
return el.getAsLong();
}
if (parameter.getParameterType().isAssignableFrom(Integer.class)) {
return el.getAsInt();
}
if (parameter.getParameterType().isAssignableFrom(String.class)) {
return el.getAsString();
}
throw new TimiException(TimiCode.ERROR, "not support parameter type");
}
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.spring.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 应用在 Controller 接口上
*
* <p>需要验证令牌,只验证该令牌是否有效,不验证数据和令牌的关系
*
* @author 夜雨
* @version 2021-08-16 17:58
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RequiredToken {
}

View File

@ -0,0 +1,56 @@
package com.imyeyu.spring.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.annotation.Annotation;
/**
* 抽象验证令牌
*
* @author 夜雨
* @version 2021-08-16 18:07
*/
public abstract class RequiredTokenAbstractInterceptor<A extends Annotation> implements HandlerInterceptor {
private final Class<A> annotation;
public RequiredTokenAbstractInterceptor(Class<A> annotation) {
this.annotation = annotation;
}
/**
* 处理前
*
* @param req 请求
* @param resp 返回
* @param handler 处理方法
* @return true 为通过
*/
@Override
public boolean preHandle(@NonNull HttpServletRequest req,
@NonNull HttpServletResponse resp,
@NonNull Object handler) {
// 方法注解
if (handler instanceof HandlerMethod handlerMethod) {
A requiredTokenAnnotation = handlerMethod.getMethodAnnotation(annotation);
if (requiredTokenAnnotation == null) {
return true;
}
return beforeRun(req, resp);
}
return true;
}
/**
* 访问前(通过 Token 限制)
*
* @param req 请求
* @param resp 返回
* @return true 为通过
*/
protected abstract boolean beforeRun(HttpServletRequest req, HttpServletResponse resp);
}

View File

@ -0,0 +1,2 @@
/** 注解和注解实现 */
package com.imyeyu.spring.annotation;

View File

@ -0,0 +1,26 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.util.SQLProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自动生成 UUID在 {@link SQLProvider} 代理器中的方法会对此注解字段自动生成 UUID
*
* @author 夜雨
* @since 2025-02-05 23:44
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoUUID {
/**
* 是否全大写 UUID
*
* @return true 为使用全大写 UUID
*/
boolean upper() default false;
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.util.SQLProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 指定列名,在 {@link SQLProvider} 代理器中的方法无法简单将字段转为数据库列名时,使用此注解指定
*
* @author 夜雨
* @since 2025-01-29 09:53
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
String value();
}

View File

@ -0,0 +1,25 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.util.SQLProvider;
import org.apache.ibatis.builder.annotation.ProviderContext;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记为 ID 字段,在 {@link SQLProvider} 代理器中的方法使用此字段进行以下操作
* <ul>
* <li>{@link SQLProvider#select(ProviderContext, Object)}</li>
* <li>{@link SQLProvider#delete(ProviderContext, Object)}</li>
* <li>{@link SQLProvider#destroy(ProviderContext, Object)}</li>
* </ul>
*
* @author 夜雨
* @since 2025-01-29 09:54
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id {
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.util.SQLProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 指定表名,在 {@link SQLProvider} 代理器中的方法无法简单将实体类名转为数据库表名时,使用此注解指定
*
* @author 夜雨
* @since 2025-01-29 09:53
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
String value();
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.spring.annotation.table;
import com.imyeyu.spring.util.SQLProvider;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* 忽略字段或表示非表实体,在 {@link SQLProvider} 代理器中的方法无需处理该字段时,使用此注解标记,不标记的字段均视为映射数据库列
*
* @author 夜雨
* @since 2025-02-06 17:46
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Transient {
}

View File

@ -0,0 +1,51 @@
package com.imyeyu.spring.bean;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 含验证码数据实体
*
* @author 夜雨
* @version 2021-03-01 17:10
*/
public class CaptchaData<T> {
/** 来源 */
@NotBlank(message = "timijava.code.request_bad")
protected String from;
/** 验证码 */
@NotBlank(message = "captcha.miss")
protected String captcha;
/** 数据体 */
@Valid
@NotNull
protected T data;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getCaptcha() {
return captcha;
}
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}

View File

@ -0,0 +1,83 @@
package com.imyeyu.spring.bean;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.LinkedHashMap;
/**
* 抽象页面查询参数
*
* @author 夜雨
* @version 2023-06-02 14:47
*/
public class Page {
/** 下标 */
@Min(value = 0, message = "page.min_index")
protected int index = 0;
/** 数据量 */
@Max(value = 64, message = "page.max_size")
protected int size = 16;
/** 关键字 */
protected String keyword;
protected LinkedHashMap<String, BaseMapper.OrderType> orderMap;
public Page() {
}
public Page(int index, int size) {
this.index = index;
this.size = size;
}
public long getOffset() {
return (long) index * size;
}
public int getLimit() {
return size;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() {
return orderMap;
}
public void setOrderMap(LinkedHashMap<String, BaseMapper.OrderType> orderMap) {
this.orderMap = orderMap;
}
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
result.setList(pageMapper.list(page.getOffset(), page.getLimit()));
result.setTotal(pageMapper.count());
return result;
}
}

View File

@ -0,0 +1,59 @@
package com.imyeyu.spring.bean;
import com.imyeyu.java.TimiJava;
import com.imyeyu.utils.Calc;
import java.util.List;
/**
* 抽象页面查询结果
*
* @author 夜雨
* @version 2023-06-02 14:47
*/
public class PageResult<T> {
/** 总数据量 */
protected long total;
/** 总页数 */
protected int pages;
protected List<T> list;
/**
* 获取总数据量
*
* @return 总数据量
*/
public long getTotal() {
return total;
}
/**
* 设置总数据量
*
* @param total 总数据量
*/
public void setTotal(long total) {
this.total = total;
if (TimiJava.isNotEmpty(list)) {
pages = Calc.ceil(1D * total / list.size());
}
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
if (TimiJava.isNotEmpty(list)) {
pages = Calc.ceil(1D * total / list.size());
}
}
public int getPages() {
return pages;
}
}

View File

@ -0,0 +1,157 @@
package com.imyeyu.spring.bean;
/**
* RedisConfig 配置参数
*
* @author 夜雨
* @version 2021-11-21 10:02
*/
public class RedisConfigParams {
/** 地址 */
private String host;
/** 端口 */
private int port;
/** 密码 */
private String password;
/** 超时(毫秒) */
private int timeout;
/** 最大活跃连接 */
private int maxActive;
/** 最小空闲连接 */
private int minIdle;
/** 最大空闲连接 */
private int maxIdle;
/**
* 获取地址
*
* @return 地址
*/
public String getHost() {
return host;
}
/**
* 设置地址
*
* @param host 地址
*/
public void setHost(String host) {
this.host = host;
}
/**
* 获取端口
*
* @return 端口
*/
public int getPort() {
return port;
}
/**
* 设置端口
*
* @param port 端口
*/
public void setPort(int port) {
this.port = port;
}
/**
* 获取密码
*
* @return 密码
*/
public String getPassword() {
return password;
}
/**
* 设置密码
*
* @param password 密码
*/
public void setPassword(String password) {
this.password = password;
}
/**
* 获取超时(毫秒)
*
* @return 超时(毫秒)
*/
public int getTimeout() {
return timeout;
}
/**
* 设置超时(毫秒)
*
* @param timeout 超时(毫秒)
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* 获取最大活跃连接
*
* @return 最大活跃连接
*/
public int getMaxActive() {
return maxActive;
}
/**
* 设置最大活跃连接
*
* @param maxActive 最大活跃连接
*/
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
/**
* 获取最小空闲连接
*
* @return 最小空闲连接
*/
public int getMinIdle() {
return minIdle;
}
/**
* 设置最小空闲连接
*
* @param minIdle 最小空闲连接
*/
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
/**
* 获取最大空闲连接
*
* @return 最大空闲连接
*/
public int getMaxIdle() {
return maxIdle;
}
/**
* 设置最大空闲连接
*
* @param maxIdle 最大空闲连接
*/
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
}

View File

@ -0,0 +1,2 @@
/** 配置对象 */
package com.imyeyu.spring.bean;

View File

@ -0,0 +1,123 @@
package com.imyeyu.spring.config;
import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.util.Redis;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
/**
* 抽象 RedisConfig
*
* @author 夜雨
* @version 2021-11-21 10:00
*/
public abstract class AbstractRedisConfig implements CachingConfigurer {
/**
* 构建 Redis 基本配置
*
* @return Redis 基本配置
*/
protected abstract RedisConfigParams configParams();
/**
* 连接池配置
* <p>参考:
* <pre>
* GenericObjectPoolConfig&lt;?&gt; config = new GenericObjectPoolConfig&lt;&gt;();
* config.setMaxTotal(config.getMaxActive());
* config.setMinIdle(config.getMinIdle());
* config.setMaxIdle(config.getMaxIdle());
* config.setMaxWait(Duration.ofMillis(config.getMaxWait()));
* </pre>
*
* @return GenericObjectPoolConfig
*/
public abstract GenericObjectPoolConfig<?> getPoolConfig();
/**
* Redis key 生成策略
* <p>参考:
* <pre>
* return (target, method, params) -> {
* StringBuilder sb = new StringBuilder();
* sb.append(target.getClass().getName());
* sb.append(method.getName());
* for (Object obj : params) {
* sb.append(obj.toString());
* }
* return sb.toString();
* };
* </pre>
*
* @return KeyGenerator
*/
public abstract KeyGenerator keyGenerator();
/**
* 构造自定义 RedisTemplate
*
* @param database 数据库
* @param keySerializer 键序列化方式
* @param <K> 键类型
* @param <V> 值类型
* @return RedisTemplate
*/
public <K, V> Redis<K, V> getRedis(int database, RedisSerializer<K> keySerializer) {
return getRedis(database, keySerializer, null);
}
/**
* 构造自定义 RedisTemplate
* <p>针对同一服务器不同数据库
*
* @param database 数据库
* @param keySerializer 键序列化方式
* @param valueSerializer 值序列化方式
* @param <K> 键类型
* @param <V> 值类型
* @return RedisTemplate
*/
public <K, V> Redis<K, V> getRedis(int database, RedisSerializer<K> keySerializer, RedisSerializer<V> valueSerializer) {
RedisConfigParams configParams = configParams();
// 构建 Redis 配置
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
// 连接参数
config.setHostName(configParams.getHost());
config.setPort(configParams.getPort());
config.setPassword(RedisPassword.of(configParams.getPassword()));
config.setDatabase(database);
// 构造连接池
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(configParams.getTimeout())).poolConfig(getPoolConfig()).build();
// 重构连接工厂
LettuceConnectionFactory factory = new LettuceConnectionFactory(config, clientConfig);
// 设置数据库
factory.afterPropertiesSet();
RedisTemplate<K, V> rt = new RedisTemplate<>();
rt.setConnectionFactory(factory);
rt.setKeySerializer(keySerializer);
rt.setHashKeySerializer(keySerializer);
if (valueSerializer != null) {
rt.setValueSerializer(valueSerializer);
rt.setHashValueSerializer(valueSerializer);
}
rt.setConnectionFactory(factory);
rt.afterPropertiesSet();
return new Redis<>(rt, keySerializer);
}
}

View File

@ -0,0 +1,2 @@
/** 配置 */
package com.imyeyu.spring.config;

View File

@ -0,0 +1,82 @@
package com.imyeyu.spring.entity;
import com.imyeyu.utils.Time;
import java.io.Serializable;
/**
* 基本实体
*
* @author 夜雨
* @version 2021-11-20 17:45
*/
public class BaseEntity implements Serializable, Creatable, Updatable, Deletable {
/** 创建时间 */
protected Long createdAt;
/** 更新时间 */
protected Long updatedAt;
/** 删除时间 */
protected Long deletedAt;
/**
* 获取创建时间
*
* @return 创建时间
*/
public Long getCreatedAt() {
return createdAt;
}
/**
* 设置创建时间
*
* @param createdAt 创建时间
*/
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
/**
* 获取更新时间
*
* @return 更新时间
*/
public Long getUpdatedAt() {
return updatedAt;
}
/**
* 设置更新时间
*
* @param updatedAt 更新时间
*/
public void setUpdatedAt(Long updatedAt) {
this.updatedAt = updatedAt;
}
/**
* 获取删除时间
*
* @return 删除时间
*/
public Long getDeletedAt() {
return deletedAt;
}
/**
* 设置删除时间
*
* @param deletedAt 删除时间
*/
public void setDeletedAt(Long deletedAt) {
this.deletedAt = deletedAt;
}
@Override
public boolean isDeleted() {
return deletedAt != null && deletedAt < Time.now();
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.spring.entity;
/**
* 可创建实体
*
* @author 夜雨
* @since 2025-02-06 15:30
*/
public interface Creatable {
/**
* 获取创建时间
*
* @return 创建时间
*/
Long getCreatedAt();
/**
* 设置创建时间
*
* @param createdAt 创建时间
*/
void setCreatedAt(Long createdAt);
}

View File

@ -0,0 +1,31 @@
package com.imyeyu.spring.entity;
/**
* 可软删除实体
*
* @author 夜雨
* @since 2025-02-06 15:30
*/
public interface Deletable {
/**
* 获取删除时间
*
* @return 删除时间
*/
Long getDeletedAt();
/**
* 设置删除时间
*
* @param deletedAt 删除时间
*/
void setDeletedAt(Long deletedAt);
/**
* 是否已删除
*
* @return true 为已删除
*/
boolean isDeleted();
}

View File

@ -0,0 +1,12 @@
package com.imyeyu.spring.entity;
import com.imyeyu.spring.util.SQLProvider;
/**
* 可销毁实体,仅用于 {@link SQLProvider} 标记
*
* @author 夜雨
* @since 2025-02-06 18:28
*/
public interface Destroyable {
}

View File

@ -0,0 +1,34 @@
package com.imyeyu.spring.entity;
import com.imyeyu.spring.annotation.table.Id;
/**
* 基本长整型 ID 实体
*
* @author 夜雨
* @since 2025-02-07 12:51
*/
public class Entity extends BaseEntity implements IDEntity<Long> {
/** ID */
@Id
protected Long id;
/**
* 获取 ID
*
* @return ID
*/
public Long getId() {
return id;
}
/**
* 设置 ID
*
* @param id ID
*/
public void setId(Long id) {
this.id = id;
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.spring.entity;
/**
* ID 实体
*
* @author 夜雨
* @since 2025-02-07 17:10
*/
public interface IDEntity<T> {
/**
* 获取 ID
*
* @return ID
*/
T getId();
/**
* 设置 ID
*
* @param id ID
*/
void setId(T id);
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.spring.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
/**
* 基本 UUID 实体
*
* @author 夜雨
* @since 2025-02-07 12:07
*/
public class UUIDEntity extends BaseEntity implements IDEntity<String> {
/** ID */
@Id
@AutoUUID
protected String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.spring.entity;
/**
* 可更新实体
*
* @author 夜雨
* @since 2025-02-06 15:30
*/
public interface Updatable {
/**
* 获取更新时间
*
* @return 更新时间
*/
Long getUpdatedAt();
/**
* 设置更新时间
*
* @param updatedAt 更新时间
*/
void setUpdatedAt(Long updatedAt);
}

View File

@ -0,0 +1,2 @@
/** 基本实体 */
package com.imyeyu.spring.entity;

View File

@ -0,0 +1,104 @@
package com.imyeyu.spring.mapper;
import com.imyeyu.spring.util.SQLProvider;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import java.util.List;
/**
* 基本 SQL 映射,子接口可以不实现
*
* @author 夜雨
* @version 2021-07-16 09:40
*/
public interface BaseMapper<T, P> {
/**
* 排序方式
*
* @author 夜雨
* @version 2023-09-05 22:14
*/
enum OrderType {
ASC,
DESC
}
static final String NOT_DELETE = " AND `deleted_at` IS NULL ";
static final String LIMIT_1 = " LIMIT 1";
static final String UNIX_TIME = " FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) ";
static final String PAGE = NOT_DELETE + " LIMIT #{offset}, #{limit}";
/**
* 统计数据量
*
* @return 数据量
*/
long count();
/**
* 获取部分数据
*
* @param offset 偏移
* @param limit 数据量
* @return 数据列表
*/
List<T> list(long offset, int limit);
/**
* 创建数据。默认自增主键为 id如需修改请重写此接口
*
* @param t 数据对象
*/
@InsertProvider(type = SQLProvider.class, method = "insert")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(T t);
/**
* 根据 ID 获取对象
*
* @param id 索引
* @return 数据对象
*/
@SelectProvider(type = SQLProvider.class, method = "select")
T select(P id);
@SelectProvider(type = SQLProvider.class, method = "selectByExample")
T selectByExample(T t);
@SelectProvider(type = SQLProvider.class, method = "selectAllByExample")
List<T> selectAllByExample(T t);
/**
* 修改数据
*
* @param t 数据对象
*/
@UpdateProvider(type = SQLProvider.class, method = "update")
void update(T t);
/**
* 软删除
*
* @param id 索引
*/
@UpdateProvider(type = SQLProvider.class, method = "delete")
void delete(P id);
/**
* 销毁(物理删除)
*
* @param id 索引
*/
@DeleteProvider(type = SQLProvider.class, method = "destroy")
void destroy(P id);
}

View File

@ -0,0 +1,2 @@
/** 基本 MyBatis 映射 */
package com.imyeyu.spring.mapper;

View File

@ -0,0 +1,62 @@
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.mapper.BaseMapper;
/**
* 抽象实体服务类
*
* @param <T> 实体类型
* @param <P> 实体主键类型
* @author 夜雨
* @since 2025-05-14 17:06
*/
public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
/** 基本 Mapper */
protected BaseMapper<T, P> baseMapper;
/** @return Mapper 实例 */
protected abstract BaseMapper<T, P> mapper();
/** 检查 mapper */
private void checkMapper() {
if (baseMapper == null) {
baseMapper = mapper();
}
TimiException.required(baseMapper, "mapper() can not be null");
}
@Override
public PageResult<T> page(Page page) {
checkMapper();
return Page.toResult(baseMapper, page, new PageResult<>());
}
public void create(T t) {
checkMapper();
baseMapper.insert(t);
}
public T get(P id) {
checkMapper();
return baseMapper.select(id);
}
public void update(T t) {
checkMapper();
baseMapper.update(t);
}
public void delete(P p) {
checkMapper();
baseMapper.delete(p);
}
public void destroy(P p) {
checkMapper();
baseMapper.destroy(p);
}
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.spring.service;
/**
* 基本实体服务接口
* <p>实现类方法顺序:
* <ol>
* <li>基本接口 CRUD</li>
* <li>自定义接口 CRUD</li>
* <li>内部方法</li>
* </ol>
*
* @param <T> 交互实体
* @param <P> 交互实体类型
* @author 夜雨
* @version 2021-02-23 21:32
*/
public interface BaseService<T, P> extends PageableService<T>, CreatableService<T>, GettableService<T, P>, UpdatableService<T>, DeletableService<P>, DestroyableService<P> {
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.spring.service;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 可创建实体服务
*
* @param <T> 数据类型
* @author 夜雨
* @since 2025-05-14 17:29
*/
public interface CreatableService<T> {
/**
* 创建数据
*
* @param t 数据对象
* @throws TimiException 服务异常
*/
void create(T t);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.spring.service;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 可软删除实体服务
*
* @author 夜雨
* @since 2025-05-14 17:30
*/
public interface DeletableService<P> {
/**
* 软删除
*
* @param p 数据对象
* @throws TimiException 服务异常
*/
void delete(P p);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.spring.service;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 可销毁(物理删除)实体服务
*
* @author 夜雨
* @since 2025-05-14 17:30
*/
public interface DestroyableService<P> {
/**
* 销毁(物理删除)
*
* @param p 数据对象
* @throws TimiException 服务异常
*/
void destroy(P p);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.spring.service;
/**
* 可获取实体服务
*
* @param <T> 实体类型
* @param <P> 主键类型
* @author 夜雨
* @since 2025-05-14 17:31
*/
public interface GettableService<T, P> {
/**
* 获取实体
*
* @param p 主键
* @return 实体
*/
T get(P p);
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.spring.service;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
/**
* 可分页实体服务
*
* @param <T> 数据类型
* @author 夜雨
* @since 2025-05-14 17:30
*/
public interface PageableService<T> {
/**
* 分页查询
*
* @param page 页面查询参数
* @return 查询页面结果
*/
PageResult<T> page(Page page);
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.spring.service;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 可更新实体服务
*
* @param <T> 数据类型
* @author 夜雨
* @since 2025-05-14 17:30
*/
public interface UpdatableService<T> {
/**
* 修改数据
*
* @param t 数据对象
* @throws TimiException 服务异常
*/
void update(T t);
}

View File

@ -0,0 +1,2 @@
/** 基本服务 */
package com.imyeyu.spring.service;

View File

@ -0,0 +1,35 @@
package com.imyeyu.spring.util;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import com.imyeyu.java.TimiJava;
import java.lang.annotation.Annotation;
/**
* 数据验证动态消息返回抽象类
*
* @author 夜雨
* @version 2023-05-07 00:08
*/
public abstract class AbstractValidator<A extends Annotation, T> implements ConstraintValidator<A, T> {
/**
* 验证处理器,入参验证数据,返回错误消息语言映射,返回 null 时表示通过验证
*
* @param t 验证数据
* @return 验证消息回调
*/
protected abstract String inspector(T t);
@Override
public boolean isValid(T value, ConstraintValidatorContext context) {
String msgKey = inspector(value);
if (TimiJava.isNotEmpty(msgKey)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(msgKey).addConstraintViolation();
return false;
}
return true;
}
}

View File

@ -0,0 +1,103 @@
package com.imyeyu.spring.util;
import jakarta.servlet.ServletException;
import jakarta.validation.ValidationException;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.bean.timi.TimiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*
* @author 夜雨
* @version 2023-05-06 16:28
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final String DEV_LANG_CONFIG = "dev.lang";
@Value("${spring.profiles.active}")
private String env;
/**
* @param e
* @return
*/
@ExceptionHandler(HttpMessageConversionException.class)
public TimiResponse<?> conversionException(HttpMessageConversionException e) {
log.warn(e.getMessage());
if (env.contains("dev") || log.isDebugEnabled()) {
log.error("conversion error", e);
}
return new TimiResponse<>(TimiCode.ARG_BAD);
}
/**
* 请求异常
*
* @param e 异常
* @return 异常返回
*/
@ExceptionHandler(ServletException.class)
public TimiResponse<?> headerException(ServletException e) {
log.warn(e.getMessage());
if (env.contains("dev") || log.isDebugEnabled()) {
log.error("header error", e);
}
return new TimiResponse<>(TimiCode.REQUEST_BAD);
}
/**
* 接口入参基本校验异常
*
* @param e 异常
* @return 异常返回
*/
@ExceptionHandler({BindException.class, ValidationException.class, MethodArgumentNotValidException.class, TypeMismatchException.class})
public TimiResponse<?> paramsException(Exception e) {
if (e instanceof MethodArgumentNotValidException subE) {
log.warn("request error", e);
FieldError error = subE.getBindingResult().getFieldError();
if (error != null) {
return new TimiResponse<>(TimiCode.ARG_BAD, error.getDefaultMessage());
}
}
if (env.startsWith("dev") || log.isDebugEnabled()) {
log.error("request error", e);
}
return new TimiResponse<>(TimiCode.REQUEST_BAD);
}
/**
* 全局异常
*
* @param e 异常
* @return 异常返回
*/
@ExceptionHandler(Throwable.class)
public TimiResponse<?> error(Throwable e) {
if (e instanceof TimiException timiE) {
// TODO 400 以下即使是开发环境也不算异常
if (env.startsWith("dev") || log.isDebugEnabled()) {
log.error(timiE.getMessage(), e);
}
// 一般异常
return timiE.toResponse();
}
// 致命异常
log.error("fatal error", e);
return new TimiResponse<>(TimiCode.ERROR);
}
}

View File

@ -0,0 +1,75 @@
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.TimiResponse;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLogInterceptor;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Objects;
/**
* 全局返回处理器,包装 TimiResponse
*
* @author 夜雨
* @version 2023-04-30 00:59
*/
@RestControllerAdvice
public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
private CallbackArgReturn<String, String> multilingualHeader;
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return Objects.requireNonNull(returnType.getMethod()).getAnnotation(IgnoreGlobalReturn.class) == null;
}
@Override
public Object beforeBodyWrite(
Object body,
@NonNull MethodParameter returnType,
@NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
@NonNull ServerHttpRequest request,
@NonNull ServerHttpResponse response)
{
TimiResponse<?> result;
if (body instanceof TimiResponse<?> timiResponse) {
// 可能已被全局异常包装
result = timiResponse;
} else {
result = new TimiResponse<>(TimiCode.SUCCESS, body);
}
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
result.setMsg(multilingualHeader.handler(result.getMsgKey()));
} else if (TimiJava.isEmpty(result.getMsg())) {
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
}
if (30000 < result.getCode()) {
log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg());
}
return result;
}
public CallbackArgReturn<String, String> getMultilingualHeader() {
return multilingualHeader;
}
public void setMultilingualHeader(CallbackArgReturn<String, String> multilingualHeader) {
this.multilingualHeader = multilingualHeader;
}
}

View File

@ -0,0 +1,295 @@
package com.imyeyu.spring.util;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* RedisTemplate 功能封装,简化 Redis 操作
* <p>serializer 为该 RedisTemplate 的键的序列化操作,序列化解析器由 {@link AbstractRedisConfig} 提供
*
* @author 夜雨
* @version 2021-11-21 09:58
*/
public class Redis<K, V> {
private final RedisSerializer<K> serializer;
private final RedisTemplate<K, V> redis;
public Redis(RedisTemplate<K, V> redis, RedisSerializer<K> serializer) {
this.redis = redis;
this.serializer = serializer;
}
/**
* 获取 Redis 模板对象
*
* @return Redis 模板对象
*/
public RedisTemplate<?, ?> getRedis() {
return redis;
}
/**
* 加锁
*
* @param key
* @param value
* @param timeoutMS
* @return true 为加锁成功
*/
public boolean lock(K key, V value, long timeoutMS) {
Boolean lock = redis.opsForValue().setIfAbsent(key, value, timeoutMS, TimeUnit.MILLISECONDS);
return lock != null && lock;
}
public void releaseLock(K key) {
destroy(key);
}
/**
* 设置存活时间
*
* @param key 键
* @param ms 毫秒 TTL
*/
public void setExpire(K key, long ms) {
redis.expire(key, Duration.ofMillis(ms));
}
/**
* 获取该数据 TTL
*
* @param key 键
* @return 毫秒 TTL
*/
public long getExpire(K key) {
return Objects.requireNonNullElse(redis.getExpire(key, TimeUnit.MILLISECONDS), -1L);
}
/**
* 设置数据并保持 TTL
*
* @param key 键
* @param value 值
*/
public void setAndKeepTTL(K key, V value) {
Long expire = redis.getExpire(key, TimeUnit.MILLISECONDS);
if (expire == null || expire <= 0) {
// 判死
destroy(key);
} else {
redis.opsForValue().set(key, value, Duration.ofMillis(expire));
}
}
/**
* 设置数据
*
* @param key 键
* @param value 值
* @param ms 毫秒 TTL
*/
public void set(K key, V value, long ms) {
redis.opsForValue().set(key, value, Duration.ofMillis(ms));
}
/**
* 获取值
*
* @param key 键
* @return 值
*/
public V get(K key) {
return redis.opsForValue().get(key);
}
/**
* 获取值,强转为 String
*
* @param key 键
* @return 值
*/
public String getString(K key) {
return get(key).toString();
}
/**
* 获取值,强转为 Boolean
*
* @param key 键
* @return 值
*/
public Boolean is(K key) {
return Boolean.parseBoolean(getString(key));
}
/**
* 获取值,强转为 Boolean 并取反
*
* @param key 键
* @return 值
*/
public Boolean not(K key) {
return !is(key);
}
/**
* 是否存在
*
* @param key 键
* @return true 为存在
*/
public boolean has(K key) {
return get(key) != null;
}
/**
* 对列表添加值
*
* @param key 键
* @param value 值
*/
public void add(K key, V value) {
redis.opsForList().leftPush(key, value);
}
/**
* 对列表批量添加值
*
* @param key 键
* @param values 值
*/
@SafeVarargs
public final void addAll(K key, V... values) {
redis.opsForList().leftPushAll(key, values);
}
/**
* 获取为列表
*
* @param key 键
* @return 列表
*/
public List<V> getList(K key) {
return redis.opsForList().range(key, 0, -1);
}
/**
* 获取所有数据列表
*
* @return 所有数据列表
*/
public Map<K, List<V>> getAllList() {
Map<K, List<V>> r = new HashMap<>();
List<K> ks = keys("*");
for (int i = 0; i < ks.size(); i++) {
r.put(ks.get(i), getList(ks.get(i)));
}
return r;
}
/**
* 值为列表时查找是否存在某值
*
* @param key 键
* @param value 值
* @return true 为存在
*/
public boolean contains(K key, V value) {
return getList(key).contains(value);
}
/**
* 获取所有值
*
* @return 所有值
* @throws TimiException 异常
*/
public List<V> values() {
List<V> r = new ArrayList<>();
List<K> keys = keys("*");
for (K key : keys) {
r.add(get(key));
}
return r;
}
/**
* 获取所有数据(包括键)
*
* @return 所有数据(包括键)
*/
public Map<K, V> map() {
Map<K, V> r = new HashMap<>();
List<K> ks = keys("*");
for (int i = 0; i < ks.size(); i++) {
r.put(ks.get(i), get(ks.get(i)));
}
return r;
}
/**
* 获取符合条件的 key
*
* @param pattern 表达式
* @return keys
*/
public List<K> keys(String pattern) {
List<K> keys = new ArrayList<>();
scan(pattern, item -> {
if (item != null) {
keys.add(serializer.deserialize(item));
}
});
return keys;
}
/**
* 销毁对象
*
* @param key 键
* @return true 为成功
*/
public boolean destroy(K key) {
if (TimiJava.isNotEmpty(key) && has(key)) {
Boolean isSucceed = redis.delete(key);
return isSucceed != null && isSucceed;
}
return false;
}
/** 删库 */
public void flushAll() {
Objects.requireNonNull(redis.getConnectionFactory()).getConnection().serverCommands().flushAll();
}
/**
* scan 实现
*
* @param pattern 表达式
* @param consumer 对迭代到的 key 进行操作
*/
private void scan(String pattern, Consumer<byte[]> consumer) {
redis.execute((RedisConnection connection) -> {
try (Cursor<byte[]> cursor = connection.keyCommands().scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
cursor.forEachRemaining(consumer);
return null;
}
});
}
}

View File

@ -0,0 +1,99 @@
package com.imyeyu.spring.util;
import com.google.gson.Gson;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.nio.charset.StandardCharsets;
/**
* @author 夜雨
* @version 2023-07-17 16:20
*/
public class RedisSerializers {
/** 字符串序列化 */
public static final StringRedisSerializer STRING = new StringRedisSerializer();
/** 长整型序列化 */
public static final RedisSerializer<Integer> INTEGER = new RedisSerializer<>() {
@Override
public byte[] serialize(Integer value) throws SerializationException {
if (value == null) {
return null;
}
byte[] result = new byte[Integer.BYTES];
for (int i = Integer.BYTES - 1; 0 <= i; i--) {
result[i] = (byte) (value & 0xFF);
value >>= Byte.SIZE;
}
return result;
}
@Override
public Integer deserialize(byte[] bytes) throws SerializationException {
if (bytes == null) {
return null;
}
int result = 0;
for (int i = 0; i < Integer.BYTES; i++) {
result <<= Byte.SIZE;
result |= (bytes[i] & 0xFF);
}
return result;
}
};
/** 长整型序列化 */
public static final RedisSerializer<Long> LONG = new RedisSerializer<>() {
@Override
public byte[] serialize(Long value) throws SerializationException {
if (value == null) {
return null;
}
byte[] result = new byte[Long.BYTES];
for (int i = Long.BYTES - 1; 0 <= i; i--) {
result[i] = (byte) (value & 0xFF);
value >>= Byte.SIZE;
}
return result;
}
@Override
public Long deserialize(byte[] bytes) throws SerializationException {
if (bytes == null) {
return null;
}
long result = 0;
for (int i = 0; i < Long.BYTES; i++) {
result <<= Byte.SIZE;
result |= (bytes[i] & 0xFF);
}
return result;
}
};
/** Gson 序列化 */
public static <T> RedisSerializer<T> gsonSerializer(Class<T> clazz) {
return new RedisSerializer<>() {
private static final Gson GSON = new Gson();
@Override
public byte[] serialize(T object) throws SerializationException {
return GSON.toJson(object).getBytes(StandardCharsets.UTF_8);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null) {
return null;
}
return GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz);
}
};
}
}

View File

@ -0,0 +1,322 @@
package com.imyeyu.spring.util;
import com.imyeyu.java.TimiJava;
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.Id;
import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.Destroyable;
import com.imyeyu.spring.entity.Updatable;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.builder.annotation.ProviderContext;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 通用 Mapper SQL 代理器
*
* @author 夜雨
* @since 2025-02-05 23:34
*/
public class SQLProvider {
/** 反射缓存 */
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
/**
* 插入
* <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
*
* @param entity 实体
* @return SQL
*/
public String insert(ProviderContext context, Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
String columns = meta.fieldColumnList.stream().map(fc -> "`%s`".formatted(fc.columnName)).collect(Collectors.joining(", "));
String values = meta.fieldColumnList.stream().map(fc -> {
try {
if (fc.isAutoUUID && TimiJava.isEmpty(Ref.getFieldValue(entity, fc.field, String.class))) {
String uuid = UUID.randomUUID().toString();
if (fc.isAutoUpperUUID) {
uuid = uuid.toUpperCase();
}
Ref.setFieldValue(entity, fc.field, uuid);
}
} catch (IllegalAccessException e) {
throw new TimiException(TimiCode.ERROR).msgKey("auto set field:%s value error".formatted(fc.fieldName));
}
if (entity instanceof Creatable creatableEntity && creatableEntity.getCreatedAt() == null) {
creatableEntity.setCreatedAt(Time.now());
}
return "#{%s}".formatted(fc.fieldName);
}).collect(Collectors.joining(", "));
return "INSERT INTO `%s` (%s) VALUES (%s)".formatted(meta.table, columns, values);
}
/**
* 根据 ID 查询
*
* @param context 代理器上下文
* @param id ID
* @return SQL
*/
public String select(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM `%s` WHERE `%s` = #{%s}".formatted(meta.table, meta.idFieldColumn.columnName, id));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
return sql.append(" LIMIT 1").toString();
}
/**
* 根据实体非空字段使用等号查询
*
* @param entity 实体
* @return SQL
*/
public String selectByExample(Object entity) {
return selectAllByExample(entity) + BaseMapper.LIMIT_1;
}
/**
* 根据实体非空字段使用等号查询
*
* @param entity 实体
* @return SQL
*/
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);
})
.collect(Collectors.joining(" AND "));
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM `%s` WHERE %s".formatted(meta.table, conditionClause));
if (meta.canDelete) {
if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND ");
}
sql.append("(`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
return sql.toString();
}
/**
* 根据 ID 更新,需要实体实现 {@link Updatable}
*
* @param entity 实体
* @return SQL
*/
public String update(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
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));
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);
})
.collect(Collectors.joining(", "));
return "UPDATE `%s` SET `%s` WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
}
/**
* 根据 ID 软删除,需要实体实现 {@link Deletable}
*
* @param context 代理器上下文
* @param id ID
* @return SQL
*/
public String delete(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
TimiException.requiredTrue(meta.canDelete, "not allow soft delete for %s".formatted(meta.entityClass));
return "UPDATE `%s` SET `deleted_at` = %s WHERE `%s` = #{id}".formatted(meta.table, Time.now(), meta.idFieldColumn.columnName);
}
/**
* 硬删除,需要实体实现 {@link Destroyable}
*
* @param context 代理器上下文
* @param id ID
* @return SQL
*/
public String destroy(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
TimiException.requiredTrue(meta.canDestroy, "not allow destroy for %s".formatted(meta.entityClass));
return "DELETE FROM `%s` WHERE `%s` = #{id}".formatted(meta.table, meta.idFieldColumn.columnName);
}
/**
* 根据代理器上下文获取 Mapper 实体类元数据
*
* @param context 代理器上下文
* @return 实体类元数据
*/
private EntityMeta getEntityMeta(ProviderContext context) {
Type[] types = context.getMapperType().getGenericInterfaces();
ParameterizedType type = (ParameterizedType) types[0];
Class<?> entityClass = (Class<?>) type.getActualTypeArguments()[0];
return getEntityMeta(entityClass);
}
/**
* 获取实体类元数据
*
* @param entityClass 实体类
* @return 元数据
*/
private EntityMeta getEntityMeta(Class<?> entityClass) {
return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new);
}
/**
* 实体元数据
*
* @author 夜雨
* @since 2025-02-05 23:47
*/
private static class EntityMeta {
/** 实体类 */
final Class<?> entityClass;
/** 表名 */
final String table;
/** ID 字段 */
final FieldColumn idFieldColumn;
/** 只读的列名字段名映射Map&lt;列名,字段名&gt; */
final List<FieldColumn> fieldColumnList;
/** true 为可更新 */
final boolean canUpdate;
/** true 为可删除(软删除) */
final boolean canDelete;
/** true 为可销毁(硬删除) */
final boolean canDestroy;
public EntityMeta(Class<?> entityClass) {
this.entityClass = entityClass;
// 表名
while (entityClass.isAnnotationPresent(Transient.class)) {
entityClass = entityClass.getSuperclass();
}
Table table = entityClass.getAnnotation(Table.class);
if (table == null) {
this.table = Text.camelCase2underscore(entityClass.getSimpleName());
} else {
this.table = table.value();
TimiException.required(this.table, String.format("empty table annotation value for %s entity", entityClass.getName()));
}
List<Field> allFieldList = Ref.listAllFields(entityClass);
FieldColumn idFieldColumn = null;
List<FieldColumn> fieldColumnList = new ArrayList<>();
for (int i = 0; i < allFieldList.size(); i++) {
Field field = allFieldList.get(i);
if (field.isAnnotationPresent(Transient.class)) {
continue;
}
FieldColumn fieldColumn = new FieldColumn(field);
if (fieldColumn.isId) {
TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName()));
idFieldColumn = fieldColumn;
}
fieldColumnList.add(fieldColumn);
}
this.idFieldColumn = idFieldColumn;
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
canUpdate = Updatable.class.isAssignableFrom(entityClass);
canDelete = Deletable.class.isAssignableFrom(entityClass);
canDestroy = Destroyable.class.isAssignableFrom(entityClass);
}
}
/**
* 实体字段属性
*
* @author 夜雨
* @since 2025-02-07 09:54
*/
private static class FieldColumn {
/** 字段 */
final Field field;
/** 字段名 */
final String fieldName;
/** 列名 */
final String columnName;
/** true 为 ID */
final boolean isId;
/** true 为自动生成 UUID */
final boolean isAutoUUID;
final boolean isAutoUpperUUID;
public FieldColumn(Field field) {
this.field = field;
fieldName = field.getName();
Column column = field.getAnnotation(Column.class);
if (column == null) {
columnName = Text.camelCase2underscore(field.getName());
} else {
columnName = column.value();
TimiException.required(columnName, "empty field:%s column annotation value for %s entity".formatted(field.getName(), field.getDeclaringClass()));
}
isId = field.isAnnotationPresent(Id.class);
isAutoUUID = field.isAnnotationPresent(AutoUUID.class);
if (isAutoUUID) {
isAutoUpperUUID = field.getAnnotation(AutoUUID.class).upper();
} else {
isAutoUpperUUID = false;
}
}
}
}

View File

@ -0,0 +1,2 @@
/** 其他工具 */
package com.imyeyu.spring.util;

View File

@ -0,0 +1,18 @@
package test;
import java.util.List;
/**
* @author 夜雨
* @since 2025-02-06 23:36
*/
public class Main {
public static void main(String[] args) {
List<String> list = List.of("1", "2");
System.out.println(list);
list.remove(0);
list.add(0, "2");
System.out.println(list);
}
}