45 Commits

Author SHA1 Message Date
62de8e4885 Merge pull request 'v0.0.4' (#3) from dev into master
Reviewed-on: #3
2026-03-16 09:18:09 +00:00
Timi
a13795703e v0.0.4
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 21m29s
2026-03-16 17:14:18 +08:00
Timi
66e379a0bd add lombok 2026-03-16 15:42:25 +08:00
9f7460e959 Merge pull request 'v0.0.3' (#2) from dev into master
Reviewed-on: #2
2026-02-10 10:56:12 +00:00
Timi
13ae5016e8 v0.0.3
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 13s
2026-02-10 18:55:53 +08:00
90d4c5e5f6 Merge pull request 'v0.0.2' (#1) from dev into master
Reviewed-on: #1
2026-01-19 09:53:14 +00:00
Timi
d2d904fe53 add CI workflow
All checks were successful
CI/CD / build-deploy (pull_request) Successful in 33s
2026-01-19 17:52:50 +08:00
Timi
7cd79bff55 add example Logic 2026-01-15 17:22:47 +08:00
Timi
fd7bb73f5c add destroyAllByExample 2026-01-15 11:20:53 +08:00
Timi
7ac4cdae56 add DynamicTableMapper support and refactor SQL provider architecture
新增动态表名支持和 SQL 提供器架构重构,用于支持分表等动态表名场景:

- 新增 DynamicTableMapper 接口,所有方法支持显式传入表名参数
- 新增 BaseSQLProvider 基类,包含所有 SQL 构建逻辑和实体元数据管理
- 重构 SQLProvider 为 BaseSQLProvider 的适配层,专注于 BaseMapper 参数适配
- 新增 DynamicTableSQLProvider 适配层,提供动态表名的 SQL 构建能力
- 通过参数前缀 (paramPrefix) 统一处理不同的参数绑定方式
- 消除所有代码重复,SQL 构建逻辑统一到基类
- 版本升级到 0.0.2

架构优势:单一职责、零重复、易扩展、易维护

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 16:12:52 +08:00
Timi
25dd7a5eb4 add RawMapper 2026-01-05 14:55:58 +08:00
Timi
430921a16c fix BaseMapper.NOT_DELETE invalid 2026-01-05 12:59:52 +08:00
Timi
77f9feb1a1 remove not usage length 2026-01-05 11:56:56 +08:00
Timi
a9156e07f4 fix javadoc warning 2026-01-04 17:27:36 +08:00
Timi
ec7f4ecaa9 update timi-io to 0.0.2 2026-01-04 17:27:27 +08:00
Timi
85009ccd5f ignored AI Agent prompt 2026-01-04 16:44:31 +08:00
Timi
e0c0db1c76 remove CLAUDE.md 2026-01-04 16:43:51 +08:00
Timi
edfbbcf11b rename BaseMapper.list* to select 2026-01-04 15:51:40 +08:00
Timi
4c1cdf0a91 fix SQLProvider.updateSelective update empty 2026-01-04 12:11:24 +08:00
Timi
1508bf7c7f fix SQLProvider example 2025-12-25 18:09:31 +08:00
Timi
e0398b3a22 rename likeExample to likesExample 2025-12-25 14:57:49 +08:00
Timi
1205946381 implements Updatable, Deletable 2025-12-24 11:22:42 +08:00
Timi
5fe610120b fix getLanguage NPE 2025-12-22 10:52:45 +08:00
Timi
4f0d2a380b add PageIgnore 2025-12-22 10:32:36 +08:00
Timi
c463ac5443 upper base lang field to timi-java for multilingual 2025-12-22 10:32:25 +08:00
Timi
d3aded669b allow update createdAt and deletedAt 2025-12-09 22:19:07 +08:00
Timi
413f376a15 fix pageExample 2025-12-09 10:21:41 +08:00
Timi
7a52560779 update Language.Enum 2025-12-08 16:57:03 +08:00
Timi
75c8f556a8 support equals or like Example for page 2025-12-08 16:56:41 +08:00
Timi
7654c3a360 ignored .claude 2025-12-08 16:54:54 +08:00
Timi
5239b469ac ignored illegal time for create and update 2025-12-08 16:07:31 +08:00
Timi
511b519925 fix SQLProvider count and page 2025-12-03 14:39:26 +08:00
Timi
595ca407b3 add CLAUDE.md prompt 2025-12-03 10:41:10 +08:00
Timi
7aadec7306 update BaseMapper.page 2025-12-03 10:40:50 +08:00
Timi
745b3acfef add BaseMapper.deleteAllByExample 2025-12-01 11:13:36 +08:00
Timi
23598242f0 improve SQLProvider selective filter 2025-12-01 11:00:03 +08:00
Timi
113af72208 add default implement for isDeleted() 2025-11-21 14:37:55 +08:00
Timi
17b20f38e6 fix SQLProvider.updateSelective 2025-11-18 15:10:46 +08:00
Timi
08aab8d5a9 add TimiSpring.getLanguageRaw 2025-11-07 23:38:50 +08:00
Timi
f887079a62 catch multilingualHeader exception 2025-11-07 16:56:40 +08:00
Timi
3283c678db fix TimiSpring.getLanguage fail on Chrome 2025-11-07 16:55:59 +08:00
Timi
3eb6bd7df5 log for not prod env 2025-11-07 15:15:41 +08:00
Timi
6a57d22366 add Multilingual common entity 2025-11-06 17:16:39 +08:00
Timi
007253f828 support get URI token for TimiSpring.getToken 2025-11-06 16:59:00 +08:00
Timi
d1728955aa fix TimiSpring.getLanguage result null for xx-XX 2025-11-06 16:34:56 +08:00
46 changed files with 2190 additions and 980 deletions

111
.gitea/workflows/ci.yml Normal file
View 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
View File

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

52
pom.xml
View File

@@ -13,10 +13,11 @@
<groupId>com.imyeyu.spring</groupId> <groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId> <artifactId>timi-spring</artifactId>
<version>0.0.1</version> <version>0.0.4</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<java.version>21</java.version>
<springboot.version>3.4.0</springboot.version> <springboot.version>3.4.0</springboot.version>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>21</maven.compiler.target>
@@ -35,29 +36,43 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version> <version>3.3.1</version>
</plugin>
<plugin>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-maven-plugin</artifactId>
<version>1.18.20.0</version>
<configuration>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
<addOutputDirectory>false</addOutputDirectory>
<encoding>UTF-8</encoding>
</configuration>
<executions> <executions>
<execution> <execution>
<id>attach-sources</id> <phase>generate-sources</phase>
<phase>package</phase>
<goals> <goals>
<goal>jar-no-fork</goal> <goal>delombok</goal>
</goals> </goals>
</execution> </execution>
</executions> </executions>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
</dependencies>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version> <version>3.11.2</version>
<executions> <configuration>
<execution> <sourcepath>${project.build.directory}/delombok</sourcepath>
<id>attach-javadocs</id> <encoding>UTF-8</encoding>
<phase>package</phase> <charset>UTF-8</charset>
<goals> <docencoding>UTF-8</docencoding>
<goal>jar</goal> </configuration>
</goals>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
@@ -137,15 +152,14 @@
<artifactId>commons-pool2</artifactId> <artifactId>commons-pool2</artifactId>
<version>2.12.0</version> <version>2.12.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency> <dependency>
<groupId>com.imyeyu.io</groupId> <groupId>com.imyeyu.io</groupId>
<artifactId>timi-io</artifactId> <artifactId>timi-io</artifactId>
<version>0.0.1</version> <version>0.0.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,25 +1,25 @@
package com.imyeyu.spring; package com.imyeyu.spring;
import com.google.gson.Gson; import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language; import com.imyeyu.java.bean.Language;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.bean.timi.TimiResponse;
import com.imyeyu.java.ref.Ref; import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.bean.RequestRange; import com.imyeyu.spring.bean.RequestRange;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger; import org.springframework.beans.BeanWrapper;
import org.slf4j.LoggerFactory; import org.springframework.beans.BeanWrapperImpl;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import java.beans.PropertyDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
@@ -35,48 +35,10 @@ import java.util.Locale;
*/ */
public class TimiSpring { public class TimiSpring {
/** 版本号 */
public static final String VERSION = "0.0.1";
private static final Logger log = LoggerFactory.getLogger(TimiSpring.class);
private static final Gson GSON = new Gson();
/** /**
* 回调数据 * 工具类禁止实例化
*
* @param response 返回
* @param resp 返回结果
*/ */
public static void render(HttpServletResponse response, TimiResponse<?> resp) { private TimiSpring() {
try {
HttpSession session = getSession();
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write(GSON.toJson(resp).getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (Exception e) {
log.error("TimiSpring.render Error", e);
}
}
/**
* 回调错误
*
* @param response 返回
* @param code 代码
* @param msgKey 消息映射键
*/
public static void renderError(HttpServletResponse response, TimiCode code, String msgKey) {
try {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write(GSON.toJson(code.toResponse().msg(msgKey)).getBytes(StandardCharsets.UTF_8));
out.flush();
out.close();
} catch (Exception e) {
log.error("TimiSpring.renderError error", e);
}
} }
/** /**
@@ -101,24 +63,50 @@ public class TimiSpring {
return getServletRequestAttributes().getRequest(); return getServletRequestAttributes().getRequest();
} }
/**
* 获取请求域名
*
* @return 请求域名
*/
public static String getDomain() { public static String getDomain() {
return getRequest().getServerName(); return getRequest().getServerName();
} }
/**
* 获取完整域名(含协议与端口)
*
* @return 完整域名
*/
public static String getFullDomain() { public static String getFullDomain() {
HttpServletRequest req = getRequest(); HttpServletRequest req = getRequest();
String port = req.getServerPort() == 80 || req.getServerPort() == 443 ? "" : ":" + req.getServerPort(); String port = req.getServerPort() == 80 || req.getServerPort() == 443 ? "" : ":" + req.getServerPort();
return "%s://%s%s".formatted(req.getScheme(), getDomain(), port); return "%s://%s%s".formatted(req.getScheme(), getDomain(), port);
} }
/**
* 获取请求 URL
*
* @return 请求 URL
*/
public static String getURL() { public static String getURL() {
return getRequest().getRequestURL().toString(); return getRequest().getRequestURL().toString();
} }
/**
* 获取请求 URI
*
* @return 请求 URI
*/
public static String getURI() { public static String getURI() {
return getRequest().getRequestURI(); return getRequest().getRequestURI();
} }
/**
* 从 URI 指定标记开始截取
*
* @param flag 标记
* @return 截取后的 URI
*/
public static String cutURIStartAt(String flag) { public static String cutURIStartAt(String flag) {
int indexOf = getURI().indexOf(flag); int indexOf = getURI().indexOf(flag);
TimiException.requiredTrue(-1 < indexOf, "not found flag: %s".formatted(flag)); TimiException.requiredTrue(-1 < indexOf, "not found flag: %s".formatted(flag));
@@ -314,52 +302,88 @@ public class TimiSpring {
return getRequest().getParameterValues(key); return getRequest().getParameterValues(key);
} }
/**
* 添加 Cookie
*
* @param cookie Cookie
*/
public static void addCookie(Cookie cookie) { public static void addCookie(Cookie cookie) {
getResponse().addCookie(cookie); getResponse().addCookie(cookie);
} }
/**
* 添加 Cookie
*
* @param key 键
* @param value 值
*/
public static void addCookie(String key, String value) { public static void addCookie(String key, String value) {
addCookie(new Cookie(key, value)); addCookie(new Cookie(key, value));
} }
/**
* 获取 Cookie
*
* @param key 键
* @return Cookie
*/
public static Cookie getCookie(String key) { public static Cookie getCookie(String key) {
Cookie[] cookies = getRequest().getCookies(); Cookie[] cookies = getRequest().getCookies();
if (cookies == null) { if (cookies == null) {
return null; return null;
} }
for (int i = 0; i < cookies.length; i++) { for (Cookie cookie : cookies) {
if (cookies[i].getName().equals(key)) { if (cookie.getName().equals(key)) {
return cookies[i]; return cookie;
} }
} }
return null; return null;
} }
/** /**
* 获取请求头的令牌,键为 Token * 获取请求令牌,键为 Token 或 token包括请求头和 URI
* *
* @return 令牌 * @return 令牌
*/ */
public static String getToken() { 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 客户端地区语言 * @return 客户端地区语言
*/ */
public static Language getLanguage() { public static Language.Enum getLanguage() {
String name = getRequestArg("lang"); String name = getRequestArg("lang");
if (TimiJava.isEmpty(name)) { if (TimiJava.isEmpty(name)) {
name = getHeader("Accept-Language"); name = getLanguageRaw();
} }
if (TimiJava.isEmpty(name)) { if (TimiJava.isNotEmpty(name)) {
name = getLocale().toString(); 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 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);
} }
/** /**
@@ -385,11 +409,23 @@ public class TimiSpring {
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
} }
/**
* 是否本地 IP
*
* @return true 为本地 IP
*/
public static boolean isLocalIP() { public static boolean isLocalIP() {
return getRequestIP().startsWith("127"); return getRequestIP().startsWith("127");
} }
public static RequestRange requestRange(long fileLength) throws IOException { /**
* 解析 Range 请求范围
*
* @param total 总数据量
* @return 请求范围
* @throws IOException IO 异常
*/
public static RequestRange getRequestRange(long total) throws IOException {
HttpServletResponse resp = getResponse(); HttpServletResponse resp = getResponse();
String range = getRequestAttrAsString("Range"); String range = getRequestAttrAsString("Range");
@@ -401,13 +437,44 @@ public class TimiSpring {
String[] ranges = rangeValue.split("-"); String[] ranges = rangeValue.split("-");
TimiException.requiredTrue(2 == ranges.length, "Invalid Range format"); TimiException.requiredTrue(2 == ranges.length, "Invalid Range format");
long start = Long.parseLong(ranges[0]); long start = Long.parseLong(ranges[0]);
long end = ranges[1].isEmpty() ? fileLength - 1 : Long.parseLong(ranges[1]); long end = ranges[1].isEmpty() ? total - 1 : Long.parseLong(ranges[1]);
// 验证范围有效性 // 验证范围有效性
if (start < 0 || fileLength <= end || end < start) { if (start < 0 || total <= end || end < start) {
resp.setHeader("Content-Range", "bytes */" + fileLength); resp.setHeader("Content-Range", "bytes */" + total);
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null; return null;
} }
return new RequestRange(start, end); return new RequestRange(start, end, total);
}
public static void copyPropertiesNotNull(Object source, Object target) {
BeanWrapper srcBean = new BeanWrapperImpl(source);
BeanWrapper targetBean = new BeanWrapperImpl(target);
for (PropertyDescriptor pd : srcBean.getPropertyDescriptors()) {
String propertyName = pd.getName();
Object srcValue = srcBean.getPropertyValue(propertyName);
if (srcValue != null && targetBean.isWritableProperty(propertyName)) {
targetBean.setPropertyValue(propertyName, srcValue);
}
}
}
public static void responseRangeStream(InputStream stream, long total) throws IOException {
HttpServletResponse resp = getResponse();
RequestRange range = getRequestRange(total);
if (range == null) {
// 完整文件
resp.setContentLengthLong(total);
resp.setStatus(HttpServletResponse.SC_OK);
IO.toOutputStream(stream, resp.getOutputStream());
} else {
// 分片文件
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), range.getTotal()));
resp.setContentLengthLong(range.getLength());
IO.toOutputStream(stream, resp.getOutputStream(), range.getStart(), range.getEnd());
resp.flushBuffer();
}
} }
} }

View File

@@ -26,12 +26,19 @@ import org.springframework.stereotype.Component;
@Component @Component
public class AOPLogInterceptor { public class AOPLogInterceptor {
/** 全局请求追踪 ID Key */
public static final String REQUEST_ID = "TIMI_SPRING_REQUEST_ID"; public static final String REQUEST_ID = "TIMI_SPRING_REQUEST_ID";
private static final Logger log = LoggerFactory.getLogger(AOPLogInterceptor.class); private static final Logger log = LoggerFactory.getLogger(AOPLogInterceptor.class);
/**
* 创建 AOP 日志拦截器
*/
public AOPLogInterceptor() {
}
/** 注入注解 */ /** 注入注解 */
@Pointcut("@annotation(annotation.com.imyeyu.spring.AOPLog)") @Pointcut("@annotation(com.imyeyu.spring.annotation.AOPLog)")
public void logPointCut() { public void logPointCut() {
} }
@@ -56,22 +63,24 @@ public class AOPLogInterceptor {
@AfterReturning(returning = "response", pointcut = "logPointCut()") @AfterReturning(returning = "response", pointcut = "logPointCut()")
public void doAfterReturning(Object response) throws Throwable { public void doAfterReturning(Object response) throws Throwable {
String msg = "ID: {} Response <- Return."; String msg = "ID: {} Response <- Return.";
if (response instanceof IDEntity<?> entity) { switch (response) {
case IDEntity<?> entity ->
// 返回实体 // 返回实体
msg += entity.getClass().getSimpleName() + "." + entity.getId(); msg += entity.getClass().getSimpleName() + "." + entity.getId();
} else if (response instanceof PageResult<?> pageResult) { case PageResult<?> pageResult -> {
// 返回数组 // 返回数组
if (pageResult.getList().isEmpty()) { if (pageResult.getList().isEmpty()) {
msg += "PageResult<?> Empty"; msg += "PageResult<?> Empty";
} else { } else {
if (pageResult.getList().get(0) == null) { if (pageResult.getList().getFirst() == null) {
msg += "PageResult<?>." + pageResult.getList().size(); msg += "PageResult<?>." + pageResult.getList().size();
} else { } else {
msg += "PageResult<" + pageResult.getList().get(0).getClass().getSimpleName() + ">[" + pageResult.getList().size() + "]"; msg += "PageResult<%s>[%s]".formatted(pageResult.getList().getFirst().getClass().getSimpleName(), pageResult.getList().size());
} }
} }
// 返回数据页 // 返回数据页
} else if (response instanceof String string) { }
case String string -> {
// 返回字符串 // 返回字符串
if (string.length() < 64) { if (string.length() < 64) {
msg += string; msg += string;
@@ -79,13 +88,14 @@ public class AOPLogInterceptor {
msg += string.substring(0, 64) + ".."; msg += string.substring(0, 64) + "..";
} }
msg = msg.replaceAll("[\\r\\n]+", ""); msg = msg.replaceAll("[\\r\\n]+", "");
} else if (response instanceof Boolean bool) { }
case Boolean bool ->
// 返回布尔值 // 返回布尔值
msg += bool; msg += bool;
} else if (response instanceof Number number) { case Number number ->
// 返回数字 // 返回数字
msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]"; msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]";
} else { case null, default -> {
// 其他对象 // 其他对象
if (TimiJava.isNotEmpty(response)) { if (TimiJava.isNotEmpty(response)) {
msg += response.getClass().getSimpleName(); msg += response.getClass().getSimpleName();
@@ -93,6 +103,7 @@ public class AOPLogInterceptor {
msg += "NULL"; msg += "NULL";
} }
} }
}
log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID)); log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID));
} }

View File

@@ -18,6 +18,12 @@ public abstract class CaptchaValidAbstractInterceptor {
private boolean enable = true; private boolean enable = true;
/**
* 创建验证码校验拦截器
*/
protected CaptchaValidAbstractInterceptor() {
}
/** 注入注解 */ /** 注入注解 */
@Pointcut("@annotation(com.imyeyu.spring.annotation.CaptchaValid)") @Pointcut("@annotation(com.imyeyu.spring.annotation.CaptchaValid)")
public void captchaPointCut() { public void captchaPointCut() {
@@ -35,8 +41,8 @@ public abstract class CaptchaValidAbstractInterceptor {
} }
if (joinPoint.getSignature() instanceof MethodSignature ms) { if (joinPoint.getSignature() instanceof MethodSignature ms) {
Object[] args = joinPoint.getArgs(); Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) { for (Object arg : args) {
if (args[i] instanceof CaptchaData<?> captchaData) { if (arg instanceof CaptchaData<?> captchaData) {
// 校验请求参数的验证码 // 校验请求参数的验证码
verify(captchaData.getCaptchaId(), captchaData.getCaptcha()); verify(captchaData.getCaptchaId(), captchaData.getCaptcha());
break; break;
@@ -45,12 +51,20 @@ public abstract class CaptchaValidAbstractInterceptor {
} }
} }
/**
* 校验验证码
*
* @param captchaId 验证码 ID
* @param captcha 验证码
*/
protected abstract void verify(String captchaId, String captcha); protected abstract void verify(String captchaId, String captcha);
/** 启用校验 */
public void enable() { public void enable() {
enable = true; enable = true;
} }
/** 禁用校验 */
public void disable() { public void disable() {
enable = false; enable = false;
} }

View File

@@ -2,6 +2,7 @@ package com.imyeyu.spring.annotation;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.NoArgsConstructor;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
@@ -12,8 +13,15 @@ import org.springframework.web.servlet.HandlerInterceptor;
* @author 夜雨 * @author 夜雨
* @version 2021-08-16 18:07 * @version 2021-08-16 18:07
*/ */
@NoArgsConstructor
public abstract class RequestRateLimitAbstractInterceptor implements HandlerInterceptor { public abstract class RequestRateLimitAbstractInterceptor implements HandlerInterceptor {
/**
* 构建接口标识
*
* @param handlerMethod 方法信息
* @return 接口标识
*/
protected String buildId(HandlerMethod handlerMethod) { protected String buildId(HandlerMethod handlerMethod) {
return handlerMethod.getMethod().getDeclaringClass().getSimpleName() + "." + handlerMethod.getMethod().getName(); return handlerMethod.getMethod().getDeclaringClass().getSimpleName() + "." + handlerMethod.getMethod().getName();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,16 @@
package com.imyeyu.spring.bean; package com.imyeyu.spring.bean;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** /**
* 含验证码数据实体 * 含验证码数据实体
* *
* @param <T> 数据体类型
* @author 夜雨 * @author 夜雨
* @version 2021-03-01 17:10 * @version 2021-03-01 17:10
*/ */
@Data
public class CaptchaData<T> { public class CaptchaData<T> {
/** 来源 */ /** 来源 */
@@ -20,28 +23,4 @@ public class CaptchaData<T> {
/** 数据体 */ /** 数据体 */
protected T data; protected T data;
public String getCaptchaId() {
return captchaId;
}
public void setCaptchaId(String captchaId) {
this.captchaId = captchaId;
}
public String getCaptcha() {
return captcha;
}
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
} }

View File

@@ -0,0 +1,16 @@
package com.imyeyu.spring.bean;
/**
* 示例连接逻辑
*
* @author 夜雨
* @since 2026-01-15 11:19
*/
public enum Logic {
/** 且 */
AND,
/** 或 */
OR
}

View File

@@ -0,0 +1,51 @@
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;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 多语言实体基类
*
* @author 夜雨
* @since 2025-10-17 15:21
*/
@Data
@EqualsAndHashCode(callSuper = true)
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;
/**
* 获取指定语言值
*
* @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);
}
}
}

View File

@@ -4,50 +4,75 @@ import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.BasePage; import com.imyeyu.java.bean.BasePage;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.utils.Text; import com.imyeyu.utils.Text;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
/** /**
* 抽象页面查询参数 * 抽象页面查询参数
* *
* @param <T> 查询示例类型
* @author 夜雨 * @author 夜雨
* @version 2023-06-02 14:47 * @version 2023-06-02 14:47
*/ */
public class Page extends BasePage { @Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
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; protected LinkedHashMap<String, BaseMapper.OrderType> orderMap;
public Page() { /**
} * 创建分页参数
*
* @param index 页码
* @param size 每页数量
*/
public Page(int index, int size) { public Page(int index, int size) {
super(index, size); super(index, size);
} }
/**
* 获取偏移量
*
* @return 偏移量
*/
public long getOffset() { public long getOffset() {
return (long) index * size; return (long) index * size;
} }
public int getLimit() { /**
* 获取限制数量
*
* @return 限制数量
*/
public long getLimit() {
return size; return size;
} }
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() { /**
return orderMap; * 添加排序字段
} *
* @param field 字段名
public void setOrderMap(LinkedHashMap<String, BaseMapper.OrderType> orderMap) { * @param orderType 排序方式
this.orderMap = orderMap; */
}
public void addOrder(String field, BaseMapper.OrderType 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); orderMap.put(Text.camelCase2underscore(field), orderType);
} }
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
result.setList(pageMapper.listOrder(page.getOffset(), page.getLimit(), page.getOrderMap()));
result.setTotal(pageMapper.count());
return result;
}
} }

View File

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

View File

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

View File

@@ -1,38 +1,34 @@
package com.imyeyu.spring.bean; package com.imyeyu.spring.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* 请求范围参数
*
* @author 夜雨 * @author 夜雨
* @since 2025-07-14 17:09 * @since 2025-07-14 17:09
*/ */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestRange { public class RequestRange {
/** 起始值 */
private long start; private long start;
/** 结束值 */
private long end; private long end;
private long length; /** 总数据量 */
private long total;
public RequestRange(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
/**
* 获取范围长度
*
* @return 范围长度
*/
public long getLength() { public long getLength() {
return end - start + 1; return end - start + 1;
} }

View File

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

View File

@@ -1,6 +1,8 @@
package com.imyeyu.spring.entity; package com.imyeyu.spring.entity;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.utils.Time; import com.imyeyu.utils.Time;
import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -10,6 +12,7 @@ import java.io.Serializable;
* @author 夜雨 * @author 夜雨
* @version 2021-11-20 17:45 * @version 2021-11-20 17:45
*/ */
@Data
public class BaseEntity implements Serializable, Creatable, Updatable, Deletable { public class BaseEntity implements Serializable, Creatable, Updatable, Deletable {
/** 创建时间 */ /** 创建时间 */
@@ -19,62 +22,9 @@ public class BaseEntity implements Serializable, Creatable, Updatable, Deletable
protected Long updatedAt; protected Long updatedAt;
/** 删除时间 */ /** 删除时间 */
@DeleteColumn
protected Long deletedAt; protected Long deletedAt;
/**
* 获取创建时间
*
* @return 创建时间
*/
public Long getCreatedAt() {
return createdAt;
}
/**
* 设置创建时间
*
* @param createdAt 创建时间
*/
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
/**
* 获取更新时间
*
* @return 更新时间
*/
public Long getUpdatedAt() {
return updatedAt;
}
/**
* 设置更新时间
*
* @param updatedAt 更新时间
*/
public void setUpdatedAt(Long updatedAt) {
this.updatedAt = updatedAt;
}
/**
* 获取删除时间
*
* @return 删除时间
*/
public Long getDeletedAt() {
return deletedAt;
}
/**
* 设置删除时间
*
* @param deletedAt 删除时间
*/
public void setDeletedAt(Long deletedAt) {
this.deletedAt = deletedAt;
}
@Override @Override
public boolean isDeleted() { public boolean isDeleted() {
return deletedAt != null && deletedAt < Time.now(); return deletedAt != null && deletedAt < Time.now();

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@ package com.imyeyu.spring.entity;
import com.imyeyu.spring.annotation.table.AutoUUID; import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id; import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** /**
* 基本 UUID 实体 * 基本 UUID 实体
@@ -9,18 +11,12 @@ import com.imyeyu.spring.annotation.table.Id;
* @author 夜雨 * @author 夜雨
* @since 2025-02-07 12:07 * @since 2025-02-07 12:07
*/ */
@Data
@EqualsAndHashCode(callSuper = true)
public class UUIDEntity extends BaseEntity implements IDEntity<String> { public class UUIDEntity extends BaseEntity implements IDEntity<String> {
/** ID */ /** ID */
@Id @Id
@AutoUUID @AutoUUID
protected String id; protected String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
} }

View File

@@ -1,57 +0,0 @@
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> {
@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;
}
}

View File

@@ -0,0 +1,62 @@
package com.imyeyu.spring.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
*
*
* @author 夜雨
* @since 2026-03-16 15:42
*/
@MappedTypes(JsonNode.class)
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CLOB, JdbcType.LONGVARCHAR})
public class JsonNodeTypeHandler extends BaseTypeHandler<JsonNode> {
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JsonNode parameter, JdbcType jdbcType) throws SQLException {
try {
ps.setString(i, MAPPER.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new SQLException("Failed to serialize JsonNode to JSON", e);
}
}
@Override
public JsonNode getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parseJson(rs.getString(columnName));
}
@Override
public JsonNode getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parseJson(rs.getString(columnIndex));
}
@Override
public JsonNode getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parseJson(cs.getString(columnIndex));
}
private JsonNode parseJson(String json) throws SQLException {
if (json == null || json.isEmpty()) {
return MAPPER.createObjectNode();
}
try {
return MAPPER.readTree(json);
} catch (JsonProcessingException e) {
throw new SQLException("Failed to parse JSON to JsonNode", e);
}
}
}

View File

@@ -1,18 +1,23 @@
package com.imyeyu.spring.mapper; 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 com.imyeyu.spring.util.SQLProvider;
import org.apache.ibatis.annotations.DeleteProvider; import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider; import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Options; import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.SelectProvider; import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider; import org.apache.ibatis.annotations.UpdateProvider;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 基本 SQL 映射,子接口可以不实现 * 基本 SQL 映射
* *
* @param <T> 实体类型
* @param <P> 主键类型
* @author 夜雨 * @author 夜雨
* @version 2021-07-16 09:40 * @version 2021-07-16 09:40
*/ */
@@ -26,46 +31,66 @@ public interface BaseMapper<T, P> {
*/ */
enum OrderType { enum OrderType {
/** 升序 */
ASC, ASC,
/** 降序 */
DESC 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 数据量 * @return 数据量
*/ */
@SelectProvider(type = SQLProvider.class, method = "count") @SelectProvider(type = SQLProvider.class, method = "countByPage")
long count(); 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 数据列表 * @return 数据列表
*/ */
@SelectProvider(type = SQLProvider.class, method = "listOrder") @SelectProvider(type = SQLProvider.class, method = "selectAll")
List<T> listOrder(long offset, int limit, Map<String, OrderType> orderMap); List<T> selectAll();
@SelectProvider(type = SQLProvider.class, method = "listAll")
List<T> listAll();
/** /**
* 创建数据。默认自增主键为 id如需修改请重写此接口 * 创建数据
* *
* @param t 数据对象 * @param t 数据对象
*/ */
@@ -82,11 +107,45 @@ public interface BaseMapper<T, P> {
@SelectProvider(type = SQLProvider.class, method = "select") @SelectProvider(type = SQLProvider.class, method = "select")
T select(P id); 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") @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") @UpdateProvider(type = SQLProvider.class, method = "update")
void update(T t); void update(T t);
/**
* 选择性更新
*
* @param t 数据对象
*/
@UpdateProvider(type = SQLProvider.class, method = "updateSelective") @UpdateProvider(type = SQLProvider.class, method = "updateSelective")
void updateSelective(T t); void updateSelective(T t);
@@ -107,6 +171,24 @@ public interface BaseMapper<T, P> {
@UpdateProvider(type = SQLProvider.class, method = "delete") @UpdateProvider(type = SQLProvider.class, method = "delete")
void delete(P id); 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") @DeleteProvider(type = SQLProvider.class, method = "destroy")
void destroy(P id); 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);
} }

View 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);
}

View 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);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,16 @@
package com.imyeyu.spring.util; package com.imyeyu.spring.util;
import com.imyeyu.java.TimiJava;
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.ConstraintValidatorContext;
import com.imyeyu.java.TimiJava;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
/** /**
* 数据验证动态消息返回抽象类 * 数据验证动态消息返回抽象类
* *
* @param <A> 注解类型
* @param <T> 校验数据类型
* @author 夜雨 * @author 夜雨
* @version 2023-05-07 00:08 * @version 2023-05-07 00:08
*/ */

View File

@@ -0,0 +1,715 @@
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 lombok.Getter;
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
*/
@Getter
protected static class EntityMeta {
/** 实体类 */
final Class<?> entityClass;
/** 表名 */
final String table;
/** 查询字段映射 */
final String selectAllClause;
/** 页面查询字段映射 */
final String selectPageClause;
/** ID 字段 */
final FieldColumn idFieldColumn;
/** 只读的列名字段名映射Map&lt;列名,字段名&gt; */
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 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
*/
@Getter
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);
}
}
/**
* 是否非 ID 字段
*
* @return true 为非 ID 字段
*/
public boolean isNotId() {
return !isId();
}
}
}

View File

@@ -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);
}
}

View File

@@ -26,14 +26,21 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final String DEV_LANG_CONFIG = "dev.lang";
/**
* 创建全局异常处理器
*/
public GlobalExceptionHandler() {
}
@Value("${spring.profiles.active}") @Value("${spring.profiles.active}")
private String env; private String env;
/** /**
* @param e * 消息转换异常
* @return *
* @param e 异常
* @return 异常返回
*/ */
@ExceptionHandler(HttpMessageConversionException.class) @ExceptionHandler(HttpMessageConversionException.class)
public TimiResponse<?> conversionException(HttpMessageConversionException e) { public TimiResponse<?> conversionException(HttpMessageConversionException e) {
@@ -89,7 +96,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class) @ExceptionHandler(Throwable.class)
public TimiResponse<?> error(Throwable e) { public TimiResponse<?> error(Throwable e) {
if (e instanceof TimiException timiE) { if (e instanceof TimiException timiE) {
if (env.startsWith("dev") || log.isDebugEnabled()) { if (!env.startsWith("prod") || log.isDebugEnabled()) {
log.error(timiE.getMessage(), e); log.error(timiE.getMessage(), e);
} }
// 一般异常 // 一般异常

View File

@@ -8,6 +8,7 @@ import com.imyeyu.java.bean.timi.TimiResponse;
import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLogInterceptor; import com.imyeyu.spring.annotation.AOPLogInterceptor;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn; import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import lombok.Data;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@@ -27,13 +28,21 @@ import java.util.Objects;
* @author 夜雨 * @author 夜雨
* @version 2023-04-30 00:59 * @version 2023-04-30 00:59
*/ */
@Data
@RestControllerAdvice @RestControllerAdvice
public class GlobalReturnHandler implements ResponseBodyAdvice<Object> { public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
/** 多语言头处理回调 */
private CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader; private CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader;
/**
* 创建全局返回处理器
*/
public GlobalReturnHandler() {
}
@Override @Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) { public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return Objects.requireNonNull(returnType.getMethod()).getAnnotation(IgnoreGlobalReturn.class) == null; return Objects.requireNonNull(returnType.getMethod()).getAnnotation(IgnoreGlobalReturn.class) == null;
@@ -55,22 +64,19 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
} else { } else {
result = new TimiResponse<>(TimiCode.SUCCESS, body); result = new TimiResponse<>(TimiCode.SUCCESS, body);
} }
try {
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) { if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
result.setMsg(multilingualHeader.handler(result)); result.setMsg(multilingualHeader.handler(result));
} else if (TimiJava.isEmpty(result.getMsg())) { } else if (TimiJava.isEmpty(result.getMsg())) {
result.setMsg(TimiCode.fromCode(result.getCode()).toString()); 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()) { if (30000 < result.getCode()) {
log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg()); log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg());
} }
return result; return result;
} }
public CallbackArgReturn<LanguageMsgMapping<?>, String> getMultilingualHeader() {
return multilingualHeader;
}
public void setMultilingualHeader(CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader) {
this.multilingualHeader = multilingualHeader;
}
} }

View 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);
}
}

View File

@@ -1,8 +1,8 @@
package com.imyeyu.spring.util; package com.imyeyu.spring.util;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.config.AbstractRedisConfig;
import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -22,34 +22,19 @@ import java.util.function.Consumer;
* RedisTemplate 功能封装,简化 Redis 操作 * RedisTemplate 功能封装,简化 Redis 操作
* <p>serializer 为该 RedisTemplate 的键的序列化操作,序列化解析器由 {@link AbstractRedisConfig} 提供 * <p>serializer 为该 RedisTemplate 的键的序列化操作,序列化解析器由 {@link AbstractRedisConfig} 提供
* *
* @param <K> 键类型
* @param <V> 值类型
* @author 夜雨 * @author 夜雨
* @version 2021-11-21 09:58 * @version 2021-11-21 09:58
*/ */
public class Redis<K, V> { public record Redis<K, V>(RedisTemplate<K, V> redis, RedisSerializer<K> serializer) {
private final RedisSerializer<K> serializer;
private final RedisTemplate<K, V> redis;
public Redis(RedisTemplate<K, V> redis, RedisSerializer<K> serializer) {
this.redis = redis;
this.serializer = serializer;
}
/**
* 获取 Redis 模板对象
*
* @return Redis 模板对象
*/
public RedisTemplate<?, ?> getRedis() {
return redis;
}
/** /**
* 加锁 * 加锁
* *
* @param key * @param key
* @param value * @param value
* @param timeoutMS * @param timeoutMS 超时时间毫秒
* @return true 为加锁成功 * @return true 为加锁成功
*/ */
public boolean lock(K key, V value, long timeoutMS) { public boolean lock(K key, V value, long timeoutMS) {
@@ -57,6 +42,11 @@ public class Redis<K, V> {
return lock != null && lock; return lock != null && lock;
} }
/**
* 释放锁
*
* @param key 键
*/
public void releaseLock(K key) { public void releaseLock(K key) {
destroy(key); destroy(key);
} }
@@ -88,8 +78,8 @@ public class Redis<K, V> {
* @param value 值 * @param value 值
*/ */
public void setAndKeepTTL(K key, V value) { public void setAndKeepTTL(K key, V value) {
Long expire = redis.getExpire(key, TimeUnit.MILLISECONDS); long expire = redis.getExpire(key, TimeUnit.MILLISECONDS);
if (expire == null || expire <= 0) { if (expire <= 0) {
// 判死 // 判死
destroy(key); destroy(key);
} else { } else {
@@ -197,8 +187,8 @@ public class Redis<K, V> {
public Map<K, List<V>> getAllList() { public Map<K, List<V>> getAllList() {
Map<K, List<V>> r = new HashMap<>(); Map<K, List<V>> r = new HashMap<>();
List<K> ks = keys("*"); List<K> ks = keys("*");
for (int i = 0; i < ks.size(); i++) { for (K k : ks) {
r.put(ks.get(i), getList(ks.get(i))); r.put(k, getList(k));
} }
return r; return r;
} }
@@ -222,9 +212,9 @@ public class Redis<K, V> {
*/ */
public List<V> values() { public List<V> values() {
List<V> r = new ArrayList<>(); List<V> r = new ArrayList<>();
List<K> keys = keys("*"); List<K> ks = keys("*");
for (K key : keys) { for (K k : ks) {
r.add(get(key)); r.add(get(k));
} }
return r; return r;
} }
@@ -237,8 +227,8 @@ public class Redis<K, V> {
public Map<K, V> map() { public Map<K, V> map() {
Map<K, V> r = new HashMap<>(); Map<K, V> r = new HashMap<>();
List<K> ks = keys("*"); List<K> ks = keys("*");
for (int i = 0; i < ks.size(); i++) { for (K k : ks) {
r.put(ks.get(i), get(ks.get(i))); r.put(k, get(k));
} }
return r; return r;
} }
@@ -267,8 +257,7 @@ public class Redis<K, V> {
*/ */
public boolean destroy(K key) { public boolean destroy(K key) {
if (TimiJava.isNotEmpty(key) && has(key)) { if (TimiJava.isNotEmpty(key) && has(key)) {
Boolean isSucceed = redis.delete(key); return redis.delete(key);
return isSucceed != null && isSucceed;
} }
return false; return false;
} }

View File

@@ -1,18 +1,26 @@
package com.imyeyu.spring.util; package com.imyeyu.spring.util;
import com.google.gson.Gson; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Redis 序列化工具
*
* @author 夜雨 * @author 夜雨
* @version 2023-07-17 16:20 * @version 2023-07-17 16:20
*/ */
public class RedisSerializers { public class RedisSerializers {
/** 工具类禁止实例化 */
private RedisSerializers() {
}
/** 字符串序列化 */ /** 字符串序列化 */
public static final StringRedisSerializer STRING = new StringRedisSerializer(); public static final StringRedisSerializer STRING = new StringRedisSerializer();
@@ -76,15 +84,25 @@ public class RedisSerializers {
} }
}; };
/** Gson 序列化 */ /**
public static <T> RedisSerializer<T> gsonSerializer(Class<T> clazz) { * Json 序列化
*
* @param <T> 数据类型
* @param clazz 数据类型
* @return Redis 序列化器
*/
public static <T> RedisSerializer<T> jacksonSerializer(Class<T> clazz) {
return new RedisSerializer<>() { return new RedisSerializer<>() {
private static final Gson GSON = new Gson(); private static final ObjectMapper JACKSON = new ObjectMapper();
@Override @Override
public byte[] serialize(T object) throws SerializationException { public byte[] serialize(T object) throws SerializationException {
return GSON.toJson(object).getBytes(StandardCharsets.UTF_8); try {
return JACKSON.writeValueAsString(object).getBytes(StandardCharsets.UTF_8);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@@ -92,7 +110,11 @@ public class RedisSerializers {
if (bytes == null) { if (bytes == null) {
return null; return null;
} }
return GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz); try {
return JACKSON.readValue(new String(bytes, StandardCharsets.UTF_8), clazz);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
}; };
} }

View File

@@ -1,82 +1,57 @@
package com.imyeyu.spring.util; package com.imyeyu.spring.util;
import com.imyeyu.java.TimiJava; import com.imyeyu.spring.bean.Logic;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.spring.bean.Page;
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.mapper.BaseMapper; 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.annotations.Param;
import org.apache.ibatis.builder.annotation.ProviderContext; 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 代理器 * 通用 Mapper SQL 代理器
* <p>继承 {@link BaseSQLProvider},为 {@link BaseMapper} 提供适配层</p>
* *
* @author 夜雨 * @author 夜雨
* @since 2025-02-05 23:34 * @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<>(); * 创建 SQL 提供器
*/
public SQLProvider() {
}
public String count(ProviderContext context) { /**
* 根据 Page 对象查询数据列表
*
* @param context 代理器上下文
* @param page 分页参数
* @return SQL
*/
public String selectByPage(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder(); return buildSelectByPageSQL(meta, meta.table, page, "#{offset}", "#{limit}");
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();
} }
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 countByPage(ProviderContext context, @Param("page") Page<?> page) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder(); return buildCountByPageSQL(meta, meta.table, page);
sql.append("SELECT %s FROM `%s` WHERE 1 = 1".formatted(meta.selectAllClause, meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
if (TimiJava.isNotEmpty(orderMap)) {
sql.append(" ORDER BY ");
for (Map.Entry<String, BaseMapper.OrderType> item : orderMap.entrySet()) {
sql.append(Text.camelCase2underscore(item.getKey())).append(' ').append(item.getValue().toString());
sql.append(", ");
}
sql.deleteCharAt(sql.length() - 2);
} else {
if (meta.canCreate && !meta.canUpdate) {
sql.append(" ORDER BY created_at DESC");
}
if (meta.canCreate && meta.canUpdate) {
sql.append(" ORDER BY COALESCE(updated_at, created_at) DESC");
}
}
return sql.append(" LIMIT %s, %s".formatted(offset, limit)).toString();
} }
public String listAll(ProviderContext context) { /**
* 查询全部数据
*
* @param context 代理器上下文
* @return SQL
*/
public String selectAll(ProviderContext context) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder(); StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM %s WHERE 1 = 1".formatted(meta.table)); 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 实体 * @param entity 实体
* @return SQL * @return SQL
*/ */
public String insert(ProviderContext context, Object entity) { public String insert(ProviderContext context, Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass()); EntityMeta meta = getEntityMeta(entity.getClass());
String columns = meta.fieldColumnList.stream().map(fc -> "`%s`".formatted(fc.columnName)).collect(Collectors.joining(", ")); return buildInsertSQL(meta, meta.table, entity, "");
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);
} }
/** /**
@@ -125,119 +83,56 @@ public class SQLProvider {
*/ */
public String select(ProviderContext context, @Param("id") Object id) { public String select(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); return buildSelectByIdSQL(meta, meta.table, "id");
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();
} }
/** /**
* 根据实体非空字段使用等号查询 * 根据实体非空字段使用等号查询
* *
* @param entity 实体 * @param entity 实体
* @param logic 条件连接逻辑
* @return SQL * @return SQL
*/ */
public String selectByExample(Object entity) { public String selectByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
return selectAllByExample(entity) + BaseMapper.LIMIT_1; return selectAllByExample(entity, logic) + BaseMapper.LIMIT_1;
} }
/** /**
* 根据实体非空字段使用等号查询 * 根据实体非空字段使用等号查询
* *
* @param entity 实体 * @param entity 实体
* @param logic 条件连接逻辑
* @return SQL * @return SQL
*/ */
public String selectAllByExample(Object entity) { public String selectAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
EntityMeta meta = getEntityMeta(entity.getClass()); EntityMeta meta = getEntityMeta(entity.getClass());
String conditionClause = meta.fieldColumnList.stream() return buildSelectAllByExampleSQL(meta, meta.table, entity, "entity.", logic);
.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();
} }
/** /**
* 根据 ID 更新,需要实体实现 {@link Updatable} * 根据 ID 更新,需要实体实现 {@link com.imyeyu.spring.entity.Updatable Updatable}
* *
* @param entity 实体 * @param entity 实体
* @return SQL * @return SQL
*/ */
public String update(Object entity) { public String update(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass()); EntityMeta meta = getEntityMeta(entity.getClass());
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); return buildUpdateSQL(meta, meta.table, entity, "");
TimiException.required(meta.canUpdate, "not allow update for %s".formatted(meta.entityClass));
String setClause = meta.fieldColumnList.stream()
.filter(fc -> !fc.isId)
.map(fc -> {
if (entity instanceof Updatable updatableEntity) {
updatableEntity.setUpdatedAt(Time.now());
}
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
})
.collect(Collectors.joining(", "));
return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
} }
/** /**
* 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link Updatable} * 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link com.imyeyu.spring.entity.Updatable Updatable}
* *
* @param entity 实体 * @param entity 实体
* @return SQL * @return SQL
*/ */
public String updateSelective(Object entity) { public String updateSelective(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass()); EntityMeta meta = getEntityMeta(entity.getClass());
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); return buildUpdateSelectiveSQL(meta, meta.table, entity, "");
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);
} }
/** /**
* 根据 ID 软删除,需要实体实现 {@link Deletable} * 根据 ID 软删除,需要实体实现 {@link com.imyeyu.spring.entity.Deletable Deletable}
* *
* @param context 代理器上下文 * @param context 代理器上下文
* @param id ID * @param id ID
@@ -245,14 +140,23 @@ public class SQLProvider {
*/ */
public String delete(ProviderContext context, @Param("id") Object id) { public String delete(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); return buildDeleteSQL(meta, meta.table, "id");
TimiException.requiredTrue(meta.canDelete, "not allow soft delete for %s".formatted(meta.entityClass));
return "UPDATE `%s` SET `deleted_at` = %s WHERE `%s` = #{id}".formatted(meta.table, Time.now(), meta.idFieldColumn.columnName);
} }
/** /**
* 硬删除,需要实体实现 {@link Destroyable} * 根据示例批量逻辑删除
*
* @param 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 context 代理器上下文
* @param id ID * @param id ID
@@ -260,226 +164,18 @@ public class SQLProvider {
*/ */
public String destroy(ProviderContext context, @Param("id") Object id) { public String destroy(ProviderContext context, @Param("id") Object id) {
EntityMeta meta = getEntityMeta(context); EntityMeta meta = getEntityMeta(context);
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); return buildDestroySQL(meta, meta.table, "id");
TimiException.requiredTrue(meta.canDestroy, "not allow destroy for %s".formatted(meta.entityClass));
return "DELETE FROM `%s` WHERE `%s` = #{id}".formatted(meta.table, meta.idFieldColumn.columnName);
} }
/** /**
* 根据代理器上下文获取 Mapper 实体类元数据 * 根据示例批量销毁(物理删除)
* *
* @param context 代理器上下文 * @param entity 实体
* @return 实体类元数据 * @param logic 条件连接逻辑
* @return SQL
*/ */
protected EntityMeta getEntityMeta(ProviderContext context) { public String destroyAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
Type[] types = context.getMapperType().getGenericInterfaces(); EntityMeta meta = getEntityMeta(entity.getClass());
ParameterizedType type = (ParameterizedType) types[0]; return buildDestroyAllByExampleSQL(meta, meta.table, entity, "entity.", logic);
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);
}
/**
* 实体元数据
*
* @author 夜雨
* @since 2025-02-05 23:47
*/
protected static class EntityMeta {
/** 实体类 */
final Class<?> entityClass;
/** 表名 */
final String table;
/** 查询字段映射 */
final String selectAllClause;
/** ID 字段 */
final FieldColumn idFieldColumn;
/** 只读的列名字段名映射Map&lt;列名,字段名&gt; */
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);
}
public Class<?> getEntityClass() {
return entityClass;
}
public String getTable() {
return table;
}
public String getSelectAllClause() {
return selectAllClause;
}
public FieldColumn getIdFieldColumn() {
return idFieldColumn;
}
public List<FieldColumn> getFieldColumnList() {
return fieldColumnList;
}
public boolean canCreate() {
return canCreate;
}
public boolean canUpdate() {
return canUpdate;
}
public boolean canDelete() {
return canDelete;
}
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;
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 Field getField() {
return field;
}
public String getFieldName() {
return fieldName;
}
public String getColumnName() {
return columnName;
}
public boolean isId() {
return isId;
}
public boolean isAutoUUID() {
return isAutoUUID;
}
public boolean isAutoUpperUUID() {
return isAutoUpperUUID;
}
} }
} }

View File

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