Compare commits
45 Commits
9bcf17a118
...
v0.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 90d4c5e5f6 | |||
| d2d904fe53 | |||
| 7cd79bff55 | |||
| fd7bb73f5c | |||
| 7ac4cdae56 | |||
| 25dd7a5eb4 | |||
| 430921a16c | |||
| 77f9feb1a1 | |||
| a9156e07f4 | |||
| ec7f4ecaa9 | |||
| 85009ccd5f | |||
| e0c0db1c76 | |||
| edfbbcf11b | |||
| 4c1cdf0a91 | |||
| 1508bf7c7f | |||
| e0398b3a22 | |||
| 1205946381 | |||
| 5fe610120b | |||
| 4f0d2a380b | |||
| c463ac5443 | |||
| d3aded669b | |||
| 413f376a15 | |||
| 7a52560779 | |||
| 75c8f556a8 | |||
| 7654c3a360 | |||
| 5239b469ac | |||
| 511b519925 | |||
| 595ca407b3 | |||
| 7aadec7306 | |||
| 745b3acfef | |||
| 23598242f0 | |||
| 113af72208 | |||
| 17b20f38e6 | |||
| 08aab8d5a9 | |||
| f887079a62 | |||
| 3283c678db | |||
| 3eb6bd7df5 | |||
| 6a57d22366 | |||
| 007253f828 | |||
| d1728955aa | |||
| 1a81ac1c54 | |||
| 838c6cd6a4 | |||
| 39dd976820 | |||
| 2e67e4086d | |||
| 4de03cf60a |
111
.gitea/workflows/ci.yml
Normal file
111
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,111 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: act_runner_java
|
||||
if: ${{ github.event.pull_request.merged == true }}
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-21-openjdk
|
||||
steps:
|
||||
- name: Checkout code
|
||||
run: |
|
||||
git clone ${{ github.server_url }}/${{ github.repository }}.git .
|
||||
git checkout ${{ github.sha }}
|
||||
- name: Set up environment
|
||||
run: |
|
||||
echo "PR #${{ github.event.number }} merged into master"
|
||||
echo "Source branch: ${{ github.event.pull_request.head.ref }}"
|
||||
echo "Target branch: ${{ github.event.pull_request.base.ref }}"
|
||||
- name: Run tests
|
||||
run: |
|
||||
echo "Running test suite..."
|
||||
- name: Build project
|
||||
run: |
|
||||
mvn -B -DskipTests clean package source:jar javadoc:jar
|
||||
- name: Deploy to Nexus
|
||||
if: success()
|
||||
run: |
|
||||
if [ -z "${{ secrets.NEXUS_USERNAME }}" ] || [ -z "${{ secrets.NEXUS_PASSWORD }}" ]; then
|
||||
echo "Missing secrets.NEXUS_USERNAME or secrets.NEXUS_PASSWORD"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p ~/.m2
|
||||
cat > ~/.m2/settings.xml <<EOF
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||
<servers>
|
||||
<server>
|
||||
<id>timi-nexus</id>
|
||||
<username>${{ secrets.NEXUS_USERNAME }}</username>
|
||||
<password>${{ secrets.NEXUS_PASSWORD }}</password>
|
||||
</server>
|
||||
</servers>
|
||||
</settings>
|
||||
EOF
|
||||
version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
|
||||
artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId)
|
||||
main_jar="target/${artifact_id}-${version}.jar"
|
||||
sources_jar="target/${artifact_id}-${version}-sources.jar"
|
||||
javadoc_jar="target/${artifact_id}-${version}-javadoc.jar"
|
||||
if [ ! -f "$main_jar" ] || [ ! -f "$sources_jar" ] || [ ! -f "$javadoc_jar" ]; then
|
||||
echo "Missing build artifacts in target"
|
||||
exit 1
|
||||
fi
|
||||
mvn -B deploy:deploy-file \
|
||||
-Dfile="$main_jar" \
|
||||
-Dsources="$sources_jar" \
|
||||
-Djavadoc="$javadoc_jar" \
|
||||
-DpomFile="./pom.xml" \
|
||||
-Durl="https://nexus.imyeyu.com/repository/maven-releases/" \
|
||||
-DrepositoryId="timi-nexus" \
|
||||
-Dhttps.protocols=TLSv1.2 \
|
||||
-Djdk.tls.client.protocols=TLSv1.2
|
||||
- name: Create release
|
||||
if: ${{ success() && startsWith(github.event.pull_request.title, 'v') }}
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
|
||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
RELEASE_TAG: ${{ github.event.pull_request.title }}
|
||||
RELEASE_TARGET: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Missing secrets.RUNNER_TOKEN"
|
||||
exit 1
|
||||
fi
|
||||
api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases"
|
||||
payload=$(cat <<EOF
|
||||
{
|
||||
"tag_name": "$RELEASE_TAG",
|
||||
"name": "$RELEASE_TAG",
|
||||
"target_commitish": "$RELEASE_TARGET",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}
|
||||
EOF
|
||||
)
|
||||
response=$(curl -sS -X POST "$api_url" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload")
|
||||
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n 1 | grep -o '[0-9]*')
|
||||
if [ -z "$release_id" ] || echo "$response" | grep -q '"message"'; then
|
||||
echo "Create release failed: $response"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release created: id=$release_id"
|
||||
for asset_path in target/*.jar; do
|
||||
asset_name=$(basename "$asset_path")
|
||||
curl -sS -X POST "$api_url/$release_id/assets?name=$asset_name" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$asset_path"
|
||||
done
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -57,3 +57,6 @@ dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
|
||||
.claude
|
||||
/CLAUDE.md
|
||||
/AGENTS.md
|
||||
|
||||
22
pom.xml
22
pom.xml
@ -13,7 +13,7 @@
|
||||
|
||||
<groupId>com.imyeyu.spring</groupId>
|
||||
<artifactId>timi-spring</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.0.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
@ -35,29 +35,11 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.11.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
@ -145,7 +127,7 @@
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.io</groupId>
|
||||
<artifactId>timi-io</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@ -20,6 +20,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
@ -41,6 +42,12 @@ public class TimiSpring {
|
||||
private static final Logger log = LoggerFactory.getLogger(TimiSpring.class);
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
/**
|
||||
* 工具类禁止实例化
|
||||
*/
|
||||
private TimiSpring() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 回调数据
|
||||
*
|
||||
@ -101,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));
|
||||
@ -314,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) {
|
||||
@ -336,27 +386,49 @@ public class TimiSpring {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求头的令牌,键为 Token
|
||||
* 获取请求令牌,键为 Token 或 token,包括请求头和 URI
|
||||
*
|
||||
* @return 令牌
|
||||
*/
|
||||
public static String getToken() {
|
||||
return getHeader("Token");
|
||||
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() {
|
||||
String name = TimiSpring.getHeader("Language");
|
||||
public static Language.Enum getLanguage() {
|
||||
String name = getRequestArg("lang");
|
||||
if (TimiJava.isEmpty(name)) {
|
||||
name = TimiSpring.getLocale().toString();
|
||||
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();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TimiJava.isNotEmpty(name)) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -382,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();
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -17,5 +17,10 @@ import java.lang.annotation.Target;
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Column {
|
||||
|
||||
/**
|
||||
* 指定列名
|
||||
*
|
||||
* @return 列名
|
||||
*/
|
||||
String value();
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -17,5 +17,10 @@ import java.lang.annotation.Target;
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Table {
|
||||
|
||||
/**
|
||||
* 指定表名
|
||||
*
|
||||
* @return 表名
|
||||
*/
|
||||
String value();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
16
src/main/java/com/imyeyu/spring/bean/Logic.java
Normal file
16
src/main/java/com/imyeyu/spring/bean/Logic.java
Normal file
@ -0,0 +1,16 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
/**
|
||||
* 示例连接逻辑
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-15 11:19
|
||||
*/
|
||||
public enum Logic {
|
||||
|
||||
/** 且 */
|
||||
AND,
|
||||
|
||||
/** 或 */
|
||||
OR
|
||||
}
|
||||
93
src/main/java/com/imyeyu/spring/bean/Multilingual.java
Normal file
93
src/main/java/com/imyeyu/spring/bean/Multilingual.java
Normal file
@ -0,0 +1,93 @@
|
||||
package com.imyeyu.spring.bean;
|
||||
|
||||
import com.imyeyu.java.bean.Language;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
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 Language implements IDEntity<String>, Creatable, Updatable, Deletable {
|
||||
|
||||
/** 唯一标识 */
|
||||
@Id
|
||||
@AutoUUID
|
||||
protected String id;
|
||||
|
||||
/** 创建时间 */
|
||||
protected Long createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
protected Long updatedAt;
|
||||
|
||||
/** 删除时间 */
|
||||
protected Long deletedAt;
|
||||
|
||||
/**
|
||||
* 创建多语言实体
|
||||
*/
|
||||
public Multilingual() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定语言值
|
||||
*
|
||||
* @param language 指定语言
|
||||
* @return 值
|
||||
*/
|
||||
public String getValue(Language.Enum language) {
|
||||
try {
|
||||
return Ref.getFieldValue(this, language.toString().replace("_", ""), String.class);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreatedAt(Long createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpdatedAt(Long updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getDeletedAt() {
|
||||
return deletedAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDeletedAt(Long deletedAt) {
|
||||
this.deletedAt = deletedAt;
|
||||
}
|
||||
}
|
||||
@ -10,44 +10,159 @@ 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 Logic equalsLogic = Logic.AND;
|
||||
|
||||
/** 模糊匹配连接逻辑 */
|
||||
protected Logic likesLogic = Logic.OR;
|
||||
|
||||
/** 排序字段映射 */
|
||||
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 Logic getEqualsLogic() {
|
||||
return equalsLogic;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置精确匹配连接逻辑
|
||||
*
|
||||
* @param equalsLogic 连接逻辑
|
||||
*/
|
||||
public void setEqualsLogic(Logic equalsLogic) {
|
||||
this.equalsLogic = equalsLogic;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模糊匹配连接逻辑
|
||||
*
|
||||
* @return 连接逻辑
|
||||
*/
|
||||
public Logic getLikesLogic() {
|
||||
return likesLogic;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模糊匹配连接逻辑
|
||||
*
|
||||
* @param likesLogic 连接逻辑
|
||||
*/
|
||||
public void setLikesLogic(Logic likesLogic) {
|
||||
this.likesLogic = likesLogic;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排序映射
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,12 @@ package com.imyeyu.spring.bean;
|
||||
*/
|
||||
public class RedisConfigParams {
|
||||
|
||||
/**
|
||||
* 创建 Redis 配置参数
|
||||
*/
|
||||
public RedisConfigParams() {
|
||||
}
|
||||
|
||||
/** 地址 */
|
||||
private String host;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,12 @@ import java.time.Duration;
|
||||
* @version 2021-11-21 10:00
|
||||
*/
|
||||
public abstract class AbstractRedisConfig implements CachingConfigurer {
|
||||
|
||||
/**
|
||||
* 创建 Redis 配置
|
||||
*/
|
||||
protected AbstractRedisConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 Redis 基本配置
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,12 @@ public class Entity extends BaseEntity implements IDEntity<Long> {
|
||||
@Id
|
||||
protected Long id;
|
||||
|
||||
/**
|
||||
* 创建基础 ID 实体
|
||||
*/
|
||||
public Entity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ID
|
||||
*
|
||||
|
||||
@ -3,6 +3,7 @@ package com.imyeyu.spring.entity;
|
||||
/**
|
||||
* ID 实体
|
||||
*
|
||||
* @param <T> ID 类型
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 17:10
|
||||
*/
|
||||
|
||||
@ -16,6 +16,12 @@ public class UUIDEntity extends BaseEntity implements IDEntity<String> {
|
||||
@AutoUUID
|
||||
protected String id;
|
||||
|
||||
/**
|
||||
* 创建 UUID 实体
|
||||
*/
|
||||
public UUIDEntity() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
63
src/main/java/com/imyeyu/spring/handler/GsonHandler.java
Normal file
63
src/main/java/com/imyeyu/spring/handler/GsonHandler.java
Normal file
@ -0,0 +1,63 @@
|
||||
package com.imyeyu.spring.handler;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* MySQL JSON 数据类型处理器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-07-04 09:36
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||
return toElement(rs.getString(columnName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
return toElement(rs.getString(columnIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
return toElement(cs.getNString(columnIndex));
|
||||
}
|
||||
|
||||
private JsonElement toElement(String json) {
|
||||
if (TimiJava.isNotEmpty(json)) {
|
||||
JsonElement el = JsonParser.parseString(json);
|
||||
if (el.isJsonObject()) {
|
||||
return el.getAsJsonObject();
|
||||
}
|
||||
if (el.isJsonArray()) {
|
||||
return el.getAsJsonArray();
|
||||
}
|
||||
if (el.isJsonPrimitive()) {
|
||||
return el.getAsJsonPrimitive();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,23 @@
|
||||
package com.imyeyu.spring.mapper;
|
||||
|
||||
import com.imyeyu.spring.bean.Logic;
|
||||
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;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
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 +31,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,11 +107,45 @@ public interface BaseMapper<T, P> {
|
||||
@SelectProvider(type = SQLProvider.class, method = "select")
|
||||
T select(P id);
|
||||
|
||||
@SelectProvider(type = SQLProvider.class, method = "selectByExample")
|
||||
T selectByExample(T t);
|
||||
/**
|
||||
* 根据示例查询单条数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @return 数据对象
|
||||
*/
|
||||
default T selectByExample(T t) {
|
||||
return selectByExample(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询单条数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据对象
|
||||
*/
|
||||
@SelectProvider(type = SQLProvider.class, method = "selectByExample")
|
||||
T selectByExample(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @return 数据列表
|
||||
*/
|
||||
default List<T> selectAllByExample(T t) {
|
||||
return selectAllByExample(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据列表
|
||||
*/
|
||||
@SelectProvider(type = SQLProvider.class, method = "selectAllByExample")
|
||||
List<T> selectAllByExample(T t);
|
||||
List<T> selectAllByExample(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
@ -96,6 +155,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 +171,24 @@ public interface BaseMapper<T, P> {
|
||||
@UpdateProvider(type = SQLProvider.class, method = "delete")
|
||||
void delete(P id);
|
||||
|
||||
/**
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param t 示例对象
|
||||
*/
|
||||
default void deleteAllByExample(T t) {
|
||||
deleteAllByExample(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
*/
|
||||
@UpdateProvider(type = SQLProvider.class, method = "deleteAllByExample")
|
||||
void deleteAllByExample(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 销毁(物理删除)
|
||||
*
|
||||
@ -114,4 +196,22 @@ public interface BaseMapper<T, P> {
|
||||
*/
|
||||
@DeleteProvider(type = SQLProvider.class, method = "destroy")
|
||||
void destroy(P id);
|
||||
|
||||
/**
|
||||
* 根据示例批量销毁(物理删除)
|
||||
*
|
||||
* @param t 示例对象
|
||||
*/
|
||||
default void destroyAllByExample(T t) {
|
||||
destroyAllByExample(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量销毁(物理删除)
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
*/
|
||||
@DeleteProvider(type = SQLProvider.class, method = "destroyAllByExample")
|
||||
void destroyAllByExample(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
}
|
||||
|
||||
209
src/main/java/com/imyeyu/spring/mapper/DynamicTableMapper.java
Normal file
209
src/main/java/com/imyeyu/spring/mapper/DynamicTableMapper.java
Normal file
@ -0,0 +1,209 @@
|
||||
package com.imyeyu.spring.mapper;
|
||||
|
||||
import com.imyeyu.spring.bean.Logic;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.bean.PageResult;
|
||||
import com.imyeyu.spring.util.DynamicTableSQLProvider;
|
||||
import org.apache.ibatis.annotations.DeleteProvider;
|
||||
import org.apache.ibatis.annotations.InsertProvider;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.SelectProvider;
|
||||
import org.apache.ibatis.annotations.UpdateProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支持动态表名的 SQL 映射接口
|
||||
* <p>相比 {@link BaseMapper},所有方法都需要显式传入表名参数</p>
|
||||
*
|
||||
* @param <T> 实体类型
|
||||
* @param <P> 主键类型
|
||||
* @author 夜雨
|
||||
* @since 2026-01-07 11:00
|
||||
*/
|
||||
public interface DynamicTableMapper<T, P> {
|
||||
|
||||
/**
|
||||
* 根据 Page 对象查询数据列表
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return 数据列表
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectByPage")
|
||||
List<T> selectByPage(@Param("tableName") String tableName, @Param("page") Page<T> page);
|
||||
|
||||
/**
|
||||
* 根据 Page 对象统计数据量
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return 数据量
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "countByPage")
|
||||
long countByPage(@Param("tableName") String tableName, @Param("page") Page<T> page);
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
default PageResult<T> selectPageResult(String tableName, Page<T> page) {
|
||||
PageResult<T> result = new PageResult<>();
|
||||
result.setTotal(countByPage(tableName, page));
|
||||
result.setList(selectByPage(tableName, page));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @return 数据列表
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectAll")
|
||||
List<T> selectAll(@Param("tableName") String tableName);
|
||||
|
||||
/**
|
||||
* 创建数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 数据对象
|
||||
*/
|
||||
@InsertProvider(type = DynamicTableSQLProvider.class, method = "insert")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "entity.id")
|
||||
void insert(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||
|
||||
/**
|
||||
* 根据 ID 获取对象
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param id 索引
|
||||
* @return 数据对象
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "select")
|
||||
T select(@Param("tableName") String tableName, @Param("id") P id);
|
||||
|
||||
/**
|
||||
* 根据示例查询单条数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @return 数据对象
|
||||
*/
|
||||
default T selectByExample(String tableName, T entity) {
|
||||
return selectByExample(tableName, entity, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询单条数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据对象
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectByExample")
|
||||
T selectByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @return 数据列表
|
||||
*/
|
||||
default List<T> selectAllByExample(String tableName, T entity) {
|
||||
return selectAllByExample(tableName, entity, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据列表
|
||||
*/
|
||||
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectAllByExample")
|
||||
List<T> selectAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 修改数据
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 数据对象
|
||||
*/
|
||||
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "update")
|
||||
void update(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||
|
||||
/**
|
||||
* 选择性更新
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 数据对象
|
||||
*/
|
||||
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "updateSelective")
|
||||
void updateSelective(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||
|
||||
/**
|
||||
* 软删除
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param id 索引
|
||||
*/
|
||||
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "delete")
|
||||
void delete(@Param("tableName") String tableName, @Param("id") P id);
|
||||
|
||||
/**
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
*/
|
||||
default void deleteAllByExample(String tableName, T entity) {
|
||||
deleteAllByExample(tableName, entity, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
*/
|
||||
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "deleteAllByExample")
|
||||
void deleteAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 销毁(物理删除)
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param id 索引
|
||||
*/
|
||||
@DeleteProvider(type = DynamicTableSQLProvider.class, method = "destroy")
|
||||
void destroy(@Param("tableName") String tableName, @Param("id") P id);
|
||||
|
||||
/**
|
||||
* 根据示例批量销毁(物理删除)
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
*/
|
||||
default void destroyAllByExample(String tableName, T entity) {
|
||||
destroyAllByExample(tableName, entity, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量销毁(物理删除)
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param entity 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
*/
|
||||
@DeleteProvider(type = DynamicTableSQLProvider.class, method = "destroyAllByExample")
|
||||
void destroyAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||
}
|
||||
76
src/main/java/com/imyeyu/spring/mapper/RawMapper.java
Normal file
76
src/main/java/com/imyeyu/spring/mapper/RawMapper.java
Normal file
@ -0,0 +1,76 @@
|
||||
package com.imyeyu.spring.mapper;
|
||||
|
||||
import com.imyeyu.spring.bean.Logic;
|
||||
import com.imyeyu.spring.util.RawSQLProvider;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
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 数据对象
|
||||
*/
|
||||
default T selectByExampleRaw(T t) {
|
||||
return selectByExampleRaw(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询单条数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据对象
|
||||
*/
|
||||
@SelectProvider(type = RawSQLProvider.class, method = "selectByExample")
|
||||
T selectByExampleRaw(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @return 数据列表
|
||||
*/
|
||||
default List<T> selectAllByExampleRaw(T t) {
|
||||
return selectAllByExampleRaw(t, Logic.AND);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例查询全部数据
|
||||
*
|
||||
* @param t 示例对象
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 数据列表
|
||||
*/
|
||||
@SelectProvider(type = RawSQLProvider.class, method = "selectAllByExample")
|
||||
List<T> selectAllByExampleRaw(@Param("entity") T t, @Param("logic") Logic logic);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import com.imyeyu.java.bean.timi.TimiException;
|
||||
/**
|
||||
* 可软删除实体服务
|
||||
*
|
||||
* @param <P> 主键类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
|
||||
@ -5,6 +5,7 @@ import com.imyeyu.java.bean.timi.TimiException;
|
||||
/**
|
||||
* 可销毁(物理删除)实体服务
|
||||
*
|
||||
* @param <P> 主键类型
|
||||
* @author 夜雨
|
||||
* @since 2025-05-14 17:30
|
||||
*/
|
||||
|
||||
@ -18,5 +18,5 @@ public interface PageableService<T> {
|
||||
* @param page 页面查询参数
|
||||
* @return 查询页面结果
|
||||
*/
|
||||
PageResult<T> page(Page page);
|
||||
PageResult<T> page(Page<T> page);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
811
src/main/java/com/imyeyu/spring/util/BaseSQLProvider.java
Normal file
811
src/main/java/com/imyeyu/spring/util/BaseSQLProvider.java
Normal file
@ -0,0 +1,811 @@
|
||||
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.bean.Logic;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
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.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.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* SQL 提供器基类
|
||||
* <p>包含所有 SQL 构建逻辑和实体元数据管理</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-07 11:20
|
||||
*/
|
||||
public abstract class BaseSQLProvider {
|
||||
|
||||
/** 反射缓存 */
|
||||
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 根据代理器上下文获取 Mapper 实体类元数据
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @return 实体类元数据
|
||||
*/
|
||||
protected 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 元数据
|
||||
*/
|
||||
protected EntityMeta getEntityMeta(Class<?> entityClass) {
|
||||
return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分页查询 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @param offset 偏移量占位符
|
||||
* @param limit 限制数量占位符
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildSelectByPageSQL(EntityMeta meta, String tableName, Page<?> page, String offset, String limit) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT %s FROM `%s` WHERE 1 = 1".formatted(meta.selectPageClause, tableName));
|
||||
|
||||
// 添加软删除条件
|
||||
appendSoftDeleteCondition(sql, meta);
|
||||
|
||||
// 添加 Page 查询条件
|
||||
appendPageConditions(sql, page);
|
||||
|
||||
// 添加排序
|
||||
appendOrderBy(sql, meta, page);
|
||||
|
||||
// 添加分页限制
|
||||
sql.append(" LIMIT %s, %s".formatted(offset, limit));
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建分页统计 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildCountByPageSQL(EntityMeta meta, String tableName, Page<?> page) {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT COUNT(*) FROM `%s` WHERE 1 = 1".formatted(tableName));
|
||||
|
||||
// 添加软删除条件
|
||||
appendSoftDeleteCondition(sql, meta);
|
||||
|
||||
// 添加 Page 查询条件
|
||||
appendPageConditions(sql, page);
|
||||
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加软删除条件
|
||||
*
|
||||
* @param sql SQL 构建器
|
||||
* @param meta 实体元数据
|
||||
*/
|
||||
protected void appendSoftDeleteCondition(StringBuilder sql, EntityMeta meta) {
|
||||
if (meta.canDelete) {
|
||||
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Page 查询条件(精准查询和模糊查询)
|
||||
*
|
||||
* @param sql SQL 构建器
|
||||
* @param page 分页参数
|
||||
*/
|
||||
protected void appendPageConditions(StringBuilder sql, Page<?> page) {
|
||||
// 精准查询
|
||||
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(" %s ".formatted(page.getEqualsLogic())));
|
||||
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(" %s ".formatted(page.getLikesLogic())));
|
||||
if (TimiJava.isNotEmpty(conditionClause)) {
|
||||
sql.append(" AND (").append(conditionClause).append(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排序子句
|
||||
*
|
||||
* @param sql SQL 构建器
|
||||
* @param meta 实体元数据
|
||||
* @param page 分页参数
|
||||
*/
|
||||
protected void appendOrderBy(StringBuilder sql, EntityMeta meta, Page<?> page) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建示例查询条件子句
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param entity 示例实体
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @param logic 条件连接逻辑
|
||||
* @return 条件子句
|
||||
*/
|
||||
protected String buildExampleConditions(EntityMeta meta, Object entity, String paramPrefix, Logic logic) {
|
||||
return meta.fieldColumnList.stream()
|
||||
.filter(fc -> fc.isNotEmpty(entity))
|
||||
.map(fc -> "`%s` = #{%s%s}".formatted(fc.columnName, paramPrefix, fc.fieldName))
|
||||
.collect(Collectors.joining(" %s ".formatted(logic)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建插入 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 实体对象
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildInsertSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix) {
|
||||
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%s}".formatted(paramPrefix, fc.fieldName);
|
||||
}).collect(Collectors.joining(", "));
|
||||
return "INSERT INTO `%s` (%s) VALUES (%s)".formatted(tableName, columns, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建根据 ID 查询 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param idParam ID 参数占位符(如 "id")
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildSelectByIdSQL(EntityMeta meta, String tableName, String idParam) {
|
||||
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT %s FROM `%s` WHERE `%s` = #{%s}".formatted(
|
||||
meta.selectAllClause,
|
||||
tableName,
|
||||
meta.idFieldColumn.columnName,
|
||||
idParam
|
||||
));
|
||||
if (meta.canDelete) {
|
||||
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
||||
}
|
||||
return sql.append(" LIMIT 1").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建根据示例查询全部 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 示例实体
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildSelectAllByExampleSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix, Logic logic) {
|
||||
String conditionClause = buildExampleConditions(meta, entity, paramPrefix, logic);
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, tableName, 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建更新 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 实体对象
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildUpdateSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix) {
|
||||
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(FieldColumn::isNotId)
|
||||
.map(fc -> "`%s` = #{%s%s}".formatted(fc.columnName, paramPrefix, fc.fieldName))
|
||||
.collect(Collectors.joining(", "));
|
||||
return "UPDATE `%s` SET %s WHERE `%s` = #{%s%s}".formatted(
|
||||
tableName,
|
||||
setClause,
|
||||
meta.idFieldColumn.columnName,
|
||||
paramPrefix,
|
||||
meta.idFieldColumn.fieldName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建选择性更新 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 实体对象
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildUpdateSelectiveSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix) {
|
||||
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(FieldColumn::isNotId)
|
||||
.filter(fc -> fc.isNotNull(entity))
|
||||
.map(fc -> "`%s` = #{%s%s}".formatted(fc.columnName, paramPrefix, fc.fieldName))
|
||||
.collect(Collectors.joining(", "));
|
||||
return "UPDATE `%s` SET %s WHERE `%s` = #{%s%s}".formatted(
|
||||
tableName,
|
||||
setClause,
|
||||
meta.idFieldColumn.columnName,
|
||||
paramPrefix,
|
||||
meta.idFieldColumn.fieldName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建软删除 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param idParam ID 参数占位符(如 "id")
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildDeleteSQL(EntityMeta meta, String tableName, String idParam) {
|
||||
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` = #{%s}".formatted(
|
||||
tableName,
|
||||
Time.now(),
|
||||
meta.idFieldColumn.columnName,
|
||||
idParam
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建批量逻辑删除 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 示例实体
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildDeleteAllByExampleSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix, Logic logic) {
|
||||
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%s}".formatted(fc.columnName, paramPrefix, fc.fieldName))
|
||||
.collect(Collectors.joining(" %s ".formatted(logic)));
|
||||
StringBuilder sql = new StringBuilder("UPDATE `%s` SET `%s` = ".formatted(tableName, 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建硬删除 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param idParam ID 参数占位符(如 "id")
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildDestroySQL(EntityMeta meta, String tableName, String idParam) {
|
||||
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` = #{%s}".formatted(tableName, meta.idFieldColumn.columnName, idParam);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建批量物理删除 SQL
|
||||
*
|
||||
* @param meta 实体元数据
|
||||
* @param tableName 表名
|
||||
* @param entity 示例实体
|
||||
* @param paramPrefix 参数前缀(如 "entity."),空字符串表示无前缀
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
protected String buildDestroyAllByExampleSQL(EntityMeta meta, String tableName, Object entity, String paramPrefix, Logic logic) {
|
||||
TimiException.required(meta.canDestroy, "not allow destroy for %s".formatted(meta.entityClass));
|
||||
String destroyClause = meta.fieldColumnList.stream()
|
||||
.filter(FieldColumn::isNotId)
|
||||
.filter(fc -> fc.isNotEmpty(entity))
|
||||
.map(fc -> "`%s` = #{%s%s}".formatted(fc.columnName, paramPrefix, fc.fieldName))
|
||||
.collect(Collectors.joining(" %s ".formatted(logic)));
|
||||
return "DELETE FROM `%s` WHERE %s".formatted(tableName, destroyClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体元数据
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-05 23:47
|
||||
*/
|
||||
protected static class EntityMeta {
|
||||
|
||||
/** 实体类 */
|
||||
final Class<?> entityClass;
|
||||
|
||||
/** 表名 */
|
||||
final String table;
|
||||
|
||||
/** 查询字段映射 */
|
||||
final String selectAllClause;
|
||||
|
||||
/** 页面查询字段映射 */
|
||||
final String selectPageClause;
|
||||
|
||||
/** ID 字段 */
|
||||
final FieldColumn idFieldColumn;
|
||||
|
||||
/** 只读的列名字段名映射,Map<列名,字段名> */
|
||||
final List<FieldColumn> fieldColumnList;
|
||||
|
||||
/** true 为可创建 */
|
||||
final boolean canCreate;
|
||||
|
||||
/** true 为可更新 */
|
||||
final boolean canUpdate;
|
||||
|
||||
/** true 为可删除(软删除) */
|
||||
final boolean canDelete;
|
||||
|
||||
/** true 为可销毁(硬删除) */
|
||||
final boolean canDestroy;
|
||||
|
||||
/**
|
||||
* 创建实体元数据
|
||||
*
|
||||
* @param entityClass 实体类型
|
||||
*/
|
||||
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.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);
|
||||
canUpdate = Updatable.class.isAssignableFrom(entityClass);
|
||||
canDelete = Deletable.class.isAssignableFrom(entityClass);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体字段属性
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-07 09:54
|
||||
*/
|
||||
protected 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;
|
||||
|
||||
final boolean isDeleteColumn;
|
||||
|
||||
final DeleteColumn.Type deleteColumnType;
|
||||
|
||||
/**
|
||||
* 创建字段映射
|
||||
*
|
||||
* @param field 字段
|
||||
*/
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.spring.bean.Logic;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.builder.annotation.ProviderContext;
|
||||
|
||||
/**
|
||||
* 支持动态表名的 SQL 提供器
|
||||
* <p>继承自 {@link BaseSQLProvider},为 {@link com.imyeyu.spring.mapper.DynamicTableMapper DynamicTableMapper} 提供适配层</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2026-01-07 11:10
|
||||
*/
|
||||
public class DynamicTableSQLProvider extends BaseSQLProvider {
|
||||
|
||||
/**
|
||||
* 根据 Page 对象查询数据列表
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectByPage(ProviderContext context, @Param("tableName") String tableName, @Param("page") Page<?> page) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildSelectByPageSQL(meta, tableName, page, "#{page.offset}", "#{page.limit}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Page 对象统计数据量
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param page 分页参数
|
||||
* @return SQL
|
||||
*/
|
||||
public String countByPage(ProviderContext context, @Param("tableName") String tableName, @Param("page") Page<?> page) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildCountByPageSQL(meta, tableName, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部数据
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectAll(ProviderContext context, @Param("tableName") String tableName) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT * FROM `%s` WHERE 1 = 1".formatted(tableName));
|
||||
if (meta.canDelete) {
|
||||
sql.append(BaseMapper.NOT_DELETE);
|
||||
}
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String insert(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildInsertSQL(meta, tableName, entity, "entity.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String select(ProviderContext context, @Param("tableName") String tableName, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildSelectByIdSQL(meta, tableName, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectByExample(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
return selectAllByExample(context, tableName, entity, logic) + BaseMapper.LIMIT_1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectAllByExample(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildSelectAllByExampleSQL(meta, tableName, entity, "entity.", logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String update(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildUpdateSQL(meta, tableName, entity, "entity.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新,选择性更新非空属性
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String updateSelective(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildUpdateSelectiveSQL(meta, tableName, entity, "entity.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 软删除
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String delete(ProviderContext context, @Param("tableName") String tableName, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildDeleteSQL(meta, tableName, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String deleteAllByExample(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildDeleteAllByExampleSQL(meta, tableName, entity, "entity.", logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param id ID
|
||||
* @return SQL
|
||||
*/
|
||||
public String destroy(ProviderContext context, @Param("tableName") String tableName, @Param("id") Object id) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildDestroySQL(meta, tableName, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据示例批量物理删除
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param tableName 表名
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String destroyAllByExample(ProviderContext context, @Param("tableName") String tableName, @Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildDestroyAllByExampleSQL(meta, tableName, entity, "entity.", logic);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
@ -56,7 +64,7 @@ public class GlobalExceptionHandler {
|
||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||
log.error("header error", e);
|
||||
}
|
||||
return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.header");
|
||||
return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.request");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +79,7 @@ public class GlobalExceptionHandler {
|
||||
log.warn("request error", e);
|
||||
FieldError error = subE.getBindingResult().getFieldError();
|
||||
if (error != null) {
|
||||
return new TimiResponse<>(TimiCode.ARG_BAD, error.getDefaultMessage());
|
||||
return new TimiResponse<>(TimiCode.ARG_BAD, "[%s] %s".formatted(error.getField(), error.getDefaultMessage()));
|
||||
}
|
||||
}
|
||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
||||
@ -89,7 +97,7 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Throwable.class)
|
||||
public TimiResponse<?> error(Throwable e) {
|
||||
if (e instanceof TimiException timiE) {
|
||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
||||
if (!env.startsWith("prod") || log.isDebugEnabled()) {
|
||||
log.error(timiE.getMessage(), e);
|
||||
}
|
||||
// 一般异常
|
||||
|
||||
@ -2,6 +2,7 @@ package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||
import com.imyeyu.java.bean.LanguageMsgMapping;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
||||
import com.imyeyu.spring.TimiSpring;
|
||||
@ -31,7 +32,14 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
|
||||
|
||||
private CallbackArgReturn<String, String> multilingualHeader;
|
||||
/** 多语言头处理回调 */
|
||||
private CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader;
|
||||
|
||||
/**
|
||||
* 创建全局返回处理器
|
||||
*/
|
||||
public GlobalReturnHandler() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
@ -54,9 +62,14 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
||||
} 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())) {
|
||||
try {
|
||||
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
|
||||
result.setMsg(multilingualHeader.handler(result));
|
||||
} else if (TimiJava.isEmpty(result.getMsg())) {
|
||||
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("multilingual response error", e);
|
||||
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
||||
}
|
||||
if (30000 < result.getCode()) {
|
||||
@ -65,11 +78,21 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
||||
return result;
|
||||
}
|
||||
|
||||
public CallbackArgReturn<String, String> getMultilingualHeader() {
|
||||
/**
|
||||
* 获取多语言头处理回调
|
||||
*
|
||||
* @return 处理回调
|
||||
*/
|
||||
public CallbackArgReturn<LanguageMsgMapping<?>, String> getMultilingualHeader() {
|
||||
return multilingualHeader;
|
||||
}
|
||||
|
||||
public void setMultilingualHeader(CallbackArgReturn<String, String> multilingualHeader) {
|
||||
/**
|
||||
* 设置多语言头处理回调
|
||||
*
|
||||
* @param multilingualHeader 处理回调
|
||||
*/
|
||||
public void setMultilingualHeader(CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader) {
|
||||
this.multilingualHeader = multilingualHeader;
|
||||
}
|
||||
}
|
||||
|
||||
46
src/main/java/com/imyeyu/spring/util/RawSQLProvider.java
Normal file
46
src/main/java/com/imyeyu/spring/util/RawSQLProvider.java
Normal file
@ -0,0 +1,46 @@
|
||||
package com.imyeyu.spring.util;
|
||||
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.spring.bean.Logic;
|
||||
import com.imyeyu.spring.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
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(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
return selectAllByExample(entity, logic) + BaseMapper.LIMIT_1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String selectAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
String conditionClause = meta.fieldColumnList.stream()
|
||||
.filter(fc -> fc.isNotEmpty(entity))
|
||||
.map(fc -> "`%s` = #{entity.%s}".formatted(fc.columnName, fc.fieldName))
|
||||
.collect(Collectors.joining(" %s ".formatted(logic)));
|
||||
return "SELECT %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, meta.table, conditionClause);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<>() {
|
||||
|
||||
|
||||
@ -1,82 +1,57 @@
|
||||
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.BaseEntity;
|
||||
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.bean.Logic;
|
||||
import com.imyeyu.spring.bean.Page;
|
||||
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 代理器
|
||||
* <p>继承 {@link BaseSQLProvider},为 {@link BaseMapper} 提供适配层</p>
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2025-02-05 23:34
|
||||
*/
|
||||
public class SQLProvider {
|
||||
public class SQLProvider extends BaseSQLProvider {
|
||||
|
||||
/** 反射缓存 */
|
||||
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
public String count(ProviderContext context) {
|
||||
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()));
|
||||
}
|
||||
return sql.toString();
|
||||
/**
|
||||
* 创建 SQL 提供器
|
||||
*/
|
||||
public SQLProvider() {
|
||||
}
|
||||
|
||||
public String listOrder(ProviderContext context, @Param("offset") Long offset, @Param("limit") Integer limit, @Param("orderMap") Map<String, BaseMapper.OrderType> orderMap) {
|
||||
/**
|
||||
* 根据 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.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();
|
||||
return buildSelectByPageSQL(meta, meta.table, page, "#{offset}", "#{limit}");
|
||||
}
|
||||
|
||||
public String listAll(ProviderContext context) {
|
||||
/**
|
||||
* 根据 Page 对象统计数据量
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param page 分页参数
|
||||
* @return SQL
|
||||
*/
|
||||
public String countByPage(ProviderContext context, @Param("page") Page<?> page) {
|
||||
EntityMeta meta = getEntityMeta(context);
|
||||
return buildCountByPageSQL(meta, meta.table, page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部数据
|
||||
*
|
||||
* @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));
|
||||
@ -88,32 +63,15 @@ public class SQLProvider {
|
||||
|
||||
/**
|
||||
* 插入
|
||||
* <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
|
||||
* <p><i>不实现 {@link com.imyeyu.spring.entity.Creatable Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @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);
|
||||
return buildInsertSQL(meta, meta.table, entity, "");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,119 +83,56 @@ public class SQLProvider {
|
||||
*/
|
||||
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 %s FROM `%s` WHERE `%s` = #{%s}".formatted(meta.selectAllClause, 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();
|
||||
return buildSelectByIdSQL(meta, meta.table, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectByExample(Object entity) {
|
||||
return selectAllByExample(entity) + BaseMapper.LIMIT_1;
|
||||
public String selectByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
return selectAllByExample(entity, logic) + BaseMapper.LIMIT_1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体非空字段使用等号查询
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String selectAllByExample(Object entity) {
|
||||
public String selectAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
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 %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, 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();
|
||||
return buildSelectAllByExampleSQL(meta, meta.table, entity, "entity.", logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新,需要实体实现 {@link Updatable}
|
||||
* 根据 ID 更新,需要实体实现 {@link com.imyeyu.spring.entity.Updatable 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);
|
||||
return buildUpdateSQL(meta, meta.table, entity, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link Updatable}
|
||||
* 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link com.imyeyu.spring.entity.Updatable Updatable}
|
||||
*
|
||||
* @param entity 实体
|
||||
* @return SQL
|
||||
*/
|
||||
public String updateSelective(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));
|
||||
|
||||
if (entity instanceof BaseEntity baseEntity) {
|
||||
baseEntity.setCreatedAt(null);
|
||||
baseEntity.setDeletedAt(null);
|
||||
}
|
||||
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);
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
|
||||
return buildUpdateSelectiveSQL(meta, meta.table, entity, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 软删除,需要实体实现 {@link Deletable}
|
||||
* 根据 ID 软删除,需要实体实现 {@link com.imyeyu.spring.entity.Deletable Deletable}
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param id ID
|
||||
@ -245,14 +140,23 @@ public class SQLProvider {
|
||||
*/
|
||||
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);
|
||||
return buildDeleteSQL(meta, meta.table, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除,需要实体实现 {@link Destroyable}
|
||||
* 根据示例批量逻辑删除
|
||||
*
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
public String deleteAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
return buildDeleteAllByExampleSQL(meta, meta.table, entity, "entity.", logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 硬删除,需要实体实现 {@link com.imyeyu.spring.entity.Destroyable Destroyable}
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @param id ID
|
||||
@ -260,166 +164,18 @@ public class SQLProvider {
|
||||
*/
|
||||
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);
|
||||
return buildDestroySQL(meta, meta.table, "id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据代理器上下文获取 Mapper 实体类元数据
|
||||
* 根据示例批量销毁(物理删除)
|
||||
*
|
||||
* @param context 代理器上下文
|
||||
* @return 实体类元数据
|
||||
* @param entity 实体
|
||||
* @param logic 条件连接逻辑
|
||||
* @return SQL
|
||||
*/
|
||||
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;
|
||||
|
||||
/** 查询字段映射 */
|
||||
final String selectAllClause;
|
||||
|
||||
/** ID 字段 */
|
||||
final FieldColumn idFieldColumn;
|
||||
|
||||
/** 只读的列名字段名映射,Map<列名,字段名> */
|
||||
final List<FieldColumn> fieldColumnList;
|
||||
|
||||
/** true 为可创建 */
|
||||
final boolean canCreate;
|
||||
|
||||
/** 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);
|
||||
|
||||
StringBuilder selectAllClause = new StringBuilder();
|
||||
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;
|
||||
}
|
||||
{
|
||||
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.idFieldColumn = idFieldColumn;
|
||||
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
|
||||
canCreate = Creatable.class.isAssignableFrom(entityClass);
|
||||
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;
|
||||
}
|
||||
}
|
||||
public String destroyAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||
return buildDestroyAllByExampleSQL(meta, meta.table, entity, "entity.", logic);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
invalid.arg=无效的参数
|
||||
invalid.body=无效的请求体
|
||||
invalid.header=无效的请求头
|
||||
invalid.request=无效的请求
|
||||
service.error=服务错误
|
||||
Reference in New Issue
Block a user