Compare commits
81 Commits
c27146aa91
...
v0.0.10
| Author | SHA1 | Date | |
|---|---|---|---|
| f8beb03362 | |||
|
|
237f6d3890 | ||
| f5528fbfad | |||
|
|
31bb990d82 | ||
|
|
8a6e148d6c | ||
|
|
b94849d69c | ||
|
|
3f97bb1356 | ||
| 90c97f211b | |||
|
|
8d89c60a02 | ||
| 42dee2e4ad | |||
|
|
d77cf10b1b | ||
| 66eb6108cd | |||
|
|
3a343724a8 | ||
| 82f950e71d | |||
|
|
83b1265c0a | ||
| cceabb0c8d | |||
|
|
b16656af12 | ||
| 62de8e4885 | |||
|
|
a13795703e | ||
|
|
66e379a0bd | ||
| 9f7460e959 | |||
|
|
13ae5016e8 | ||
| 90d4c5e5f6 | |||
|
|
d2d904fe53 | ||
|
|
7cd79bff55 | ||
|
|
fd7bb73f5c | ||
|
|
7ac4cdae56 | ||
|
|
25dd7a5eb4 | ||
|
|
430921a16c | ||
|
|
77f9feb1a1 | ||
|
|
a9156e07f4 | ||
|
|
ec7f4ecaa9 | ||
|
|
85009ccd5f | ||
|
|
e0c0db1c76 | ||
|
|
edfbbcf11b | ||
|
|
4c1cdf0a91 | ||
|
|
1508bf7c7f | ||
|
|
e0398b3a22 | ||
|
|
1205946381 | ||
|
|
5fe610120b | ||
|
|
4f0d2a380b | ||
|
|
c463ac5443 | ||
|
|
d3aded669b | ||
|
|
413f376a15 | ||
|
|
7a52560779 | ||
|
|
75c8f556a8 | ||
|
|
7654c3a360 | ||
|
|
5239b469ac | ||
|
|
511b519925 | ||
|
|
595ca407b3 | ||
|
|
7aadec7306 | ||
|
|
745b3acfef | ||
|
|
23598242f0 | ||
|
|
113af72208 | ||
|
|
17b20f38e6 | ||
|
|
08aab8d5a9 | ||
|
|
f887079a62 | ||
|
|
3283c678db | ||
|
|
3eb6bd7df5 | ||
|
|
6a57d22366 | ||
|
|
007253f828 | ||
|
|
d1728955aa | ||
|
|
1a81ac1c54 | ||
|
|
838c6cd6a4 | ||
|
|
39dd976820 | ||
|
|
2e67e4086d | ||
|
|
4de03cf60a | ||
|
|
9bcf17a118 | ||
|
|
e08a50a9b2 | ||
|
|
945a2c5e9d | ||
|
|
1688666dca | ||
|
|
278bf7c59a | ||
|
|
8a7946ce01 | ||
|
|
f2689ab812 | ||
|
|
3ae1ccedb7 | ||
|
|
8de027e0c7 | ||
|
|
69d847f337 | ||
|
|
443757f501 | ||
|
|
2fc06e3851 | ||
|
|
831d36e095 | ||
|
|
39f628e71a |
111
.gitea/workflows/ci.yml
Normal file
111
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deploy:
|
||||||
|
runs-on: act_runner_java
|
||||||
|
if: ${{ github.event.pull_request.merged == true }}
|
||||||
|
env:
|
||||||
|
JAVA_HOME: /usr/lib/jvm/java-21-openjdk
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
git clone ${{ github.server_url }}/${{ github.repository }}.git .
|
||||||
|
git checkout ${{ github.sha }}
|
||||||
|
- name: Set up environment
|
||||||
|
run: |
|
||||||
|
echo "PR #${{ github.event.number }} merged into master"
|
||||||
|
echo "Source branch: ${{ github.event.pull_request.head.ref }}"
|
||||||
|
echo "Target branch: ${{ github.event.pull_request.base.ref }}"
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
echo "Running test suite..."
|
||||||
|
- name: Build project
|
||||||
|
run: |
|
||||||
|
mvn -B -DskipTests clean package source:jar javadoc:jar
|
||||||
|
- name: Deploy to Nexus
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ secrets.NEXUS_USERNAME }}" ] || [ -z "${{ secrets.NEXUS_PASSWORD }}" ]; then
|
||||||
|
echo "Missing secrets.NEXUS_USERNAME or secrets.NEXUS_PASSWORD"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p ~/.m2
|
||||||
|
cat > ~/.m2/settings.xml <<EOF
|
||||||
|
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
|
||||||
|
<servers>
|
||||||
|
<server>
|
||||||
|
<id>timi-nexus</id>
|
||||||
|
<username>${{ secrets.NEXUS_USERNAME }}</username>
|
||||||
|
<password>${{ secrets.NEXUS_PASSWORD }}</password>
|
||||||
|
</server>
|
||||||
|
</servers>
|
||||||
|
</settings>
|
||||||
|
EOF
|
||||||
|
version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
|
||||||
|
artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId)
|
||||||
|
main_jar="target/${artifact_id}-${version}.jar"
|
||||||
|
sources_jar="target/${artifact_id}-${version}-sources.jar"
|
||||||
|
javadoc_jar="target/${artifact_id}-${version}-javadoc.jar"
|
||||||
|
if [ ! -f "$main_jar" ] || [ ! -f "$sources_jar" ] || [ ! -f "$javadoc_jar" ]; then
|
||||||
|
echo "Missing build artifacts in target"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mvn -B deploy:deploy-file \
|
||||||
|
-Dfile="$main_jar" \
|
||||||
|
-Dsources="$sources_jar" \
|
||||||
|
-Djavadoc="$javadoc_jar" \
|
||||||
|
-DpomFile="./pom.xml" \
|
||||||
|
-Durl="https://nexus.imyeyu.com/repository/maven-releases/" \
|
||||||
|
-DrepositoryId="timi-nexus" \
|
||||||
|
-Dhttps.protocols=TLSv1.2 \
|
||||||
|
-Djdk.tls.client.protocols=TLSv1.2
|
||||||
|
- name: Create release
|
||||||
|
if: ${{ success() && startsWith(github.event.pull_request.title, 'v') }}
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
|
||||||
|
GITEA_SERVER_URL: ${{ github.server_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ github.repository }}
|
||||||
|
RELEASE_TAG: ${{ github.event.pull_request.title }}
|
||||||
|
RELEASE_TARGET: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$GITEA_TOKEN" ]; then
|
||||||
|
echo "Missing secrets.RUNNER_TOKEN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases"
|
||||||
|
payload=$(cat <<EOF
|
||||||
|
{
|
||||||
|
"tag_name": "$RELEASE_TAG",
|
||||||
|
"name": "$RELEASE_TAG",
|
||||||
|
"target_commitish": "$RELEASE_TARGET",
|
||||||
|
"draft": false,
|
||||||
|
"prerelease": false
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
response=$(curl -sS -X POST "$api_url" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload")
|
||||||
|
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n 1 | grep -o '[0-9]*')
|
||||||
|
if [ -z "$release_id" ] || echo "$response" | grep -q '"message"'; then
|
||||||
|
echo "Create release failed: $response"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Release created: id=$release_id"
|
||||||
|
for asset_path in target/*.jar; do
|
||||||
|
asset_name=$(basename "$asset_path")
|
||||||
|
curl -sS -X POST "$api_url/$release_id/assets?name=$asset_name" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"$asset_path"
|
||||||
|
done
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -57,3 +57,6 @@ dependency-reduced-pom.xml
|
|||||||
buildNumber.properties
|
buildNumber.properties
|
||||||
.mvn/timing.properties
|
.mvn/timing.properties
|
||||||
|
|
||||||
|
.claude
|
||||||
|
/CLAUDE.md
|
||||||
|
/AGENTS.md
|
||||||
|
|||||||
99
pom.xml
99
pom.xml
@@ -7,57 +7,101 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.4.0</version>
|
<version>3.5.11</version>
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<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.10</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<springboot.version>3.4.0</springboot.version>
|
<java.version>21</java.version>
|
||||||
|
<maven.test.skip>true</maven.test.skip>
|
||||||
|
<springboot.version>3.5.11</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>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.test.skip>true</maven.test.skip>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.1.3</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-source-plugin</artifactId>
|
||||||
|
<version>3.3.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok-maven-plugin</artifactId>
|
||||||
|
<version>1.18.20.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>21</source>
|
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
|
||||||
<target>21</target>
|
<outputDirectory>${project.build.directory}/delombok</outputDirectory>
|
||||||
|
<addOutputDirectory>false</addOutputDirectory>
|
||||||
<encoding>UTF-8</encoding>
|
<encoding>UTF-8</encoding>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>generate-sources</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>delombok</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.36</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>3.11.2</version>
|
||||||
|
<configuration>
|
||||||
|
<sourcepath>${project.build.directory}/delombok</sourcepath>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
<docencoding>UTF-8</docencoding>
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<repository>
|
||||||
|
<id>timi_nexus</id>
|
||||||
|
<url>https://nexus.imyeyu.com/repository/maven-releases/</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>timi_nexus</id>
|
||||||
|
<url>https://nexus.imyeyu.com/repository/maven-public/</url>
|
||||||
|
<releases>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
<version>${springboot.version}</version>
|
<version>${springboot.version}</version>
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
|
||||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.fasterxml.jackson.module</groupId>
|
|
||||||
<artifactId>jackson-module-parameter-names</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -94,15 +138,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.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,23 +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 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.io.OutputStream;
|
import java.beans.PropertyDescriptor;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,24 +63,56 @@ 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) {
|
||||||
|
int indexOf = getURI().indexOf(flag);
|
||||||
|
TimiException.requiredTrue(-1 < indexOf, "not found flag: %s".formatted(flag));
|
||||||
|
return getURI().substring(indexOf + flag.length());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 HttpServlet 回调
|
* 获取 HttpServlet 回调
|
||||||
*
|
*
|
||||||
@@ -286,49 +282,108 @@ public class TimiSpring {
|
|||||||
getRequest().removeAttribute(key);
|
getRequest().removeAttribute(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求 URL 参数
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @return 参数值
|
||||||
|
*/
|
||||||
|
public static String getRequestArg(String key) {
|
||||||
|
return getRequest().getParameter(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求 URL 参数(多值)
|
||||||
|
*
|
||||||
|
* @param key 键
|
||||||
|
* @return 参数值
|
||||||
|
*/
|
||||||
|
public static String[] getRequestArgs(String 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 = TimiSpring.getHeader("Language");
|
String name = getRequestArg("lang");
|
||||||
if (TimiJava.isEmpty(name)) {
|
if (TimiJava.isEmpty(name)) {
|
||||||
name = TimiSpring.getLocale().toString();
|
name = getLanguageRaw();
|
||||||
|
}
|
||||||
|
if (TimiJava.isNotEmpty(name)) {
|
||||||
|
List<Locale.LanguageRange> rangeList = Locale.LanguageRange.parse(name);
|
||||||
|
for (Locale.LanguageRange item : rangeList) {
|
||||||
|
if (item.getRange().contains("-")) {
|
||||||
|
name = item.getRange();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (TimiJava.isNotEmpty(name)) {
|
||||||
|
name = name.replace("-", "_");
|
||||||
}
|
}
|
||||||
if (TimiJava.isEmpty(name)) { // use for not support
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -354,7 +409,72 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Range 请求范围
|
||||||
|
*
|
||||||
|
* @param total 总数据量
|
||||||
|
* @return 请求范围
|
||||||
|
* @throws IOException IO 异常
|
||||||
|
*/
|
||||||
|
public static RequestRange getRequestRange(long total) throws IOException {
|
||||||
|
HttpServletResponse resp = getResponse();
|
||||||
|
|
||||||
|
String range = getRequestAttrAsString("Range");
|
||||||
|
if (range == null || !range.startsWith("bytes=")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 处理 bytes=0-999 格式
|
||||||
|
String rangeValue = range.substring("bytes=".length());
|
||||||
|
String[] ranges = rangeValue.split("-");
|
||||||
|
TimiException.requiredTrue(2 == ranges.length, "Invalid Range format");
|
||||||
|
long start = Long.parseLong(ranges[0]);
|
||||||
|
long end = ranges[1].isEmpty() ? total - 1 : Long.parseLong(ranges[1]);
|
||||||
|
// 验证范围有效性
|
||||||
|
if (start < 0 || total <= end || end < start) {
|
||||||
|
resp.setHeader("Content-Range", "bytes */" + total);
|
||||||
|
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,41 +63,45 @@ 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();
|
// 返回实体
|
||||||
} else if (response instanceof PageResult<?> pageResult) {
|
msg += entity.getClass().getSimpleName() + "." + entity.getId();
|
||||||
// 返回数组
|
case PageResult<?> pageResult -> {
|
||||||
if (pageResult.getList().isEmpty()) {
|
// 返回数组
|
||||||
msg += "PageResult<?> Empty";
|
if (pageResult.getList().isEmpty()) {
|
||||||
} else {
|
msg += "PageResult<?> Empty";
|
||||||
if (pageResult.getList().get(0) == null) {
|
|
||||||
msg += "PageResult<?>." + pageResult.getList().size();
|
|
||||||
} else {
|
} else {
|
||||||
msg += "PageResult<" + pageResult.getList().get(0).getClass().getSimpleName() + ">[" + pageResult.getList().size() + "]";
|
if (pageResult.getList().getFirst() == null) {
|
||||||
|
msg += "PageResult<?>." + pageResult.getList().size();
|
||||||
|
} else {
|
||||||
|
msg += "PageResult<%s>[%s]".formatted(pageResult.getList().getFirst().getClass().getSimpleName(), pageResult.getList().size());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 返回数据页
|
||||||
}
|
}
|
||||||
// 返回数据页
|
case String string -> {
|
||||||
} else if (response instanceof String string) {
|
// 返回字符串
|
||||||
// 返回字符串
|
if (string.length() < 64) {
|
||||||
if (string.length() < 64) {
|
msg += string;
|
||||||
msg += string;
|
} else {
|
||||||
} else {
|
msg += string.substring(0, 64) + "..";
|
||||||
msg += string.substring(0, 64) + "..";
|
}
|
||||||
|
msg = msg.replaceAll("[\\r\\n]+", "");
|
||||||
}
|
}
|
||||||
msg = msg.replaceAll("[\\r\\n]+", "");
|
case Boolean bool ->
|
||||||
} else if (response instanceof Boolean bool) {
|
// 返回布尔值
|
||||||
// 返回布尔值
|
msg += bool;
|
||||||
msg += bool;
|
case Number number ->
|
||||||
} else if (response instanceof Number number) {
|
// 返回数字
|
||||||
// 返回数字
|
msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]";
|
||||||
msg += response.getClass().getSimpleName() + ".[" + number.doubleValue() + "]";
|
case null, default -> {
|
||||||
} else {
|
// 其他对象
|
||||||
// 其他对象
|
if (TimiJava.isNotEmpty(response)) {
|
||||||
if (TimiJava.isNotEmpty(response)) {
|
msg += response.getClass().getSimpleName();
|
||||||
msg += response.getClass().getSimpleName();
|
} else {
|
||||||
} else {
|
msg += "NULL";
|
||||||
msg += "NULL";
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID));
|
log.info(msg, TimiSpring.getSessionAttr(REQUEST_ID));
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
package com.imyeyu.spring.annotation;
|
package com.imyeyu.spring.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
|
||||||
import java.lang.annotation.ElementType;
|
import java.lang.annotation.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单字段 Json 数据体
|
* 图形验证码校验注解
|
||||||
*
|
*
|
||||||
* @author 夜雨
|
* @author 夜雨
|
||||||
* @version 2023-08-09 10:36
|
* @since 2023-07-15 10:09
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.PARAMETER)
|
@Target(ElementType.METHOD)
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Documented
|
public @interface CaptchaValid {
|
||||||
public @interface RequestSingleParam {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.imyeyu.spring.annotation;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.bean.CaptchaData;
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图形验证码校验注解处理器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2023-07-15 10:01
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
public abstract class CaptchaValidAbstractInterceptor {
|
||||||
|
|
||||||
|
private boolean enable = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建验证码校验拦截器
|
||||||
|
*/
|
||||||
|
protected CaptchaValidAbstractInterceptor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入注解 */
|
||||||
|
@Pointcut("@annotation(com.imyeyu.spring.annotation.CaptchaValid)")
|
||||||
|
public void captchaPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行前
|
||||||
|
*
|
||||||
|
* @param joinPoint 切入点
|
||||||
|
*/
|
||||||
|
@Before("captchaPointCut()")
|
||||||
|
public void doBefore(JoinPoint joinPoint) {
|
||||||
|
if (!enable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (joinPoint.getSignature() instanceof MethodSignature ms) {
|
||||||
|
Object[] args = joinPoint.getArgs();
|
||||||
|
for (Object arg : args) {
|
||||||
|
if (arg instanceof CaptchaData<?> captchaData) {
|
||||||
|
// 校验请求参数的验证码
|
||||||
|
verify(captchaData.getCaptchaId(), captchaData.getCaptcha());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*
|
||||||
|
* @param captchaId 验证码 ID
|
||||||
|
* @param captcha 验证码
|
||||||
|
*/
|
||||||
|
protected abstract void verify(String captchaId, String captcha);
|
||||||
|
|
||||||
|
/** 启用校验 */
|
||||||
|
public void enable() {
|
||||||
|
enable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 禁用校验 */
|
||||||
|
public void disable() {
|
||||||
|
enable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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 请求体中的单个字段并绑定到接口参数。
|
||||||
|
*
|
||||||
|
* <p>默认使用方法参数名作为 JSON 字段名,也可以手动指定字段名。
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* public void run(@RequestBodyValue String data) {
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* public void run(@RequestBodyValue("value") Long id) {
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2026-04-08 11:00
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface RequestBodyValue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 字段名,为空时使用方法参数名。
|
||||||
|
*
|
||||||
|
* @return JSON 字段名
|
||||||
|
*/
|
||||||
|
String value() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否必须存在该字段。
|
||||||
|
*
|
||||||
|
* @return true 表示必须存在
|
||||||
|
*/
|
||||||
|
boolean required() default true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.imyeyu.spring.annotation;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RequestBodyValue} 参数解析器。
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @version 2026-04-08 11:00
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class RequestBodyValueArgumentResolver implements HandlerMethodArgumentResolver {
|
||||||
|
|
||||||
|
private static final String REQUEST_BODY_JSON_NODE_ATTR = RequestBodyValueArgumentResolver.class.getName() + ".REQUEST_BODY_JSON_NODE";
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 {@link RequestBodyValue} 参数解析器。
|
||||||
|
*
|
||||||
|
* @param objectMapper Jackson 对象映射器
|
||||||
|
*/
|
||||||
|
public RequestBodyValueArgumentResolver(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsParameter(@NonNull MethodParameter parameter) {
|
||||||
|
return parameter.hasParameterAnnotation(RequestBodyValue.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
|
||||||
|
RequestBodyValue bodyValue = parameter.getParameterAnnotation(RequestBodyValue.class);
|
||||||
|
if (bodyValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||||
|
TimiException.required(request, "not found request");
|
||||||
|
assert request != null;
|
||||||
|
|
||||||
|
JsonNode requestBody;
|
||||||
|
{
|
||||||
|
Object cached = request.getAttribute(REQUEST_BODY_JSON_NODE_ATTR);
|
||||||
|
if (cached instanceof JsonNode jsonNode) {
|
||||||
|
return jsonNode;
|
||||||
|
}
|
||||||
|
byte[] bodyBytes = request.getInputStream().readAllBytes();
|
||||||
|
TimiException.requiredTrue(1 < bodyBytes.length, "empty request body");
|
||||||
|
requestBody = objectMapper.readTree(bodyBytes);
|
||||||
|
TimiException.requiredTrue(requestBody != null && requestBody.isObject(), "not object request body");
|
||||||
|
request.setAttribute(REQUEST_BODY_JSON_NODE_ATTR, requestBody);
|
||||||
|
}
|
||||||
|
String fieldName = bodyValue.value();
|
||||||
|
{
|
||||||
|
if (TimiJava.isEmpty(fieldName)) {
|
||||||
|
fieldName = parameter.getParameterName();
|
||||||
|
}
|
||||||
|
TimiException.required(fieldName, "not found @RequestBodyValue parameter name");
|
||||||
|
}
|
||||||
|
JsonNode fieldNode = requestBody.get(fieldName);
|
||||||
|
if (fieldNode == null || fieldNode.isMissingNode() || fieldNode.isNull()) {
|
||||||
|
if (!bodyValue.required()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new TimiException(TimiCode.ARG_MISS, "not found json field: %s".formatted(fieldName));
|
||||||
|
}
|
||||||
|
return objectMapper.treeToValue(fieldNode, parameter.getParameterType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.imyeyu.spring.annotation.table;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记逻辑删除字段并指定存储类型
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2025-12-01 10:56
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface DeleteColumn {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除的存储类型
|
||||||
|
*
|
||||||
|
* @return 存储类型
|
||||||
|
*/
|
||||||
|
Type value() default Type.UNIX;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑删除的时间类型
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2025-12-01 10:57
|
||||||
|
*/
|
||||||
|
enum Type {
|
||||||
|
|
||||||
|
/** 毫秒时间戳 */
|
||||||
|
UNIX,
|
||||||
|
|
||||||
|
/** 日期 */
|
||||||
|
DATE,
|
||||||
|
|
||||||
|
/** 日期时间 */
|
||||||
|
DATE_TIME
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.imyeyu.spring.annotation.table;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.bean.Page;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 {@link com.imyeyu.spring.mapper.BaseMapper#selectPageResult(Page)} 方法忽略查询该属性
|
||||||
|
* <br />
|
||||||
|
* {@link com.imyeyu.spring.service.AbstractEntityService#page(Page)} 同上
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2025-12-12 14:54
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
public @interface PageIgnore {
|
||||||
|
}
|
||||||
@@ -17,5 +17,10 @@ import java.lang.annotation.Target;
|
|||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
public @interface Table {
|
public @interface Table {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定表名
|
||||||
|
*
|
||||||
|
* @return 表名
|
||||||
|
*/
|
||||||
String value();
|
String value();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,26 @@
|
|||||||
package com.imyeyu.spring.bean;
|
package com.imyeyu.spring.bean;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
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> {
|
||||||
|
|
||||||
/** 来源 */
|
/** 来源 */
|
||||||
@NotBlank(message = "timijava.code.request_bad")
|
@NotBlank(message = "timijava.code.request_bad")
|
||||||
protected String from;
|
protected String captchaId;
|
||||||
|
|
||||||
/** 验证码 */
|
/** 验证码 */
|
||||||
@NotBlank(message = "captcha.miss")
|
@NotBlank(message = "captcha.miss")
|
||||||
protected String captcha;
|
protected String captcha;
|
||||||
|
|
||||||
/** 数据体 */
|
/** 数据体 */
|
||||||
@Valid
|
|
||||||
@NotNull
|
|
||||||
protected T data;
|
protected T data;
|
||||||
|
|
||||||
public String getFrom() {
|
|
||||||
return from;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFrom(String from) {
|
|
||||||
this.from = from;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCaptcha() {
|
|
||||||
return captcha;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCaptcha(String captcha) {
|
|
||||||
this.captcha = captcha;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(T data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/main/java/com/imyeyu/spring/bean/Logic.java
Normal file
16
src/main/java/com/imyeyu/spring/bean/Logic.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.imyeyu.spring.bean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 示例连接逻辑
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-01-15 11:19
|
||||||
|
*/
|
||||||
|
public enum Logic {
|
||||||
|
|
||||||
|
/** 且 */
|
||||||
|
AND,
|
||||||
|
|
||||||
|
/** 或 */
|
||||||
|
OR
|
||||||
|
}
|
||||||
@@ -1,83 +1,78 @@
|
|||||||
package com.imyeyu.spring.bean;
|
package com.imyeyu.spring.bean;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Max;
|
import com.imyeyu.java.TimiJava;
|
||||||
import jakarta.validation.constraints.Min;
|
import com.imyeyu.java.bean.BasePage;
|
||||||
import com.imyeyu.spring.mapper.BaseMapper;
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
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 {
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class Page<T> extends BasePage {
|
||||||
|
|
||||||
/** 下标 */
|
/** 精确匹配示例 */
|
||||||
@Min(value = 0, message = "page.min_index")
|
protected T equalsExample;
|
||||||
protected int index = 0;
|
|
||||||
|
|
||||||
/** 数据量 */
|
/** 模糊匹配示例 */
|
||||||
@Max(value = 64, message = "page.max_size")
|
protected T likesExample;
|
||||||
protected int size = 16;
|
|
||||||
|
|
||||||
/** 关键字 */
|
/** 精确匹配连接逻辑 */
|
||||||
protected String keyword;
|
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) {
|
||||||
this.index = index;
|
super(index, size);
|
||||||
this.size = 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 int getIndex() {
|
/**
|
||||||
return index;
|
* 添加排序字段
|
||||||
}
|
*
|
||||||
|
* @param field 字段名
|
||||||
public void setIndex(int index) {
|
* @param orderType 排序方式
|
||||||
this.index = index;
|
*/
|
||||||
}
|
public void addOrder(String field, BaseMapper.OrderType orderType) {
|
||||||
|
orderMap = TimiJava.defaultIfNull(orderMap, new LinkedHashMap<>());
|
||||||
public int getSize() {
|
orderMap.put(Text.camelCase2underscore(field), orderType);
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(int size) {
|
|
||||||
this.size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKeyword() {
|
|
||||||
return keyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setKeyword(String keyword) {
|
|
||||||
this.keyword = keyword;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() {
|
|
||||||
return orderMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOrderMap(LinkedHashMap<String, BaseMapper.OrderType> orderMap) {
|
|
||||||
this.orderMap = orderMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
|
|
||||||
result.setList(pageMapper.list(page.getOffset(), page.getLimit()));
|
|
||||||
result.setTotal(pageMapper.count());
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,14 @@
|
|||||||
package com.imyeyu.spring.bean;
|
package com.imyeyu.spring.bean;
|
||||||
|
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.bean.BasePageResult;
|
||||||
import com.imyeyu.utils.Calc;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 抽象页面查询结果
|
* 抽象页面查询结果
|
||||||
*
|
*
|
||||||
|
* @param <T> 列表元素类型
|
||||||
* @author 夜雨
|
* @author 夜雨
|
||||||
* @version 2023-06-02 14:47
|
* @version 2023-06-02 14:47
|
||||||
*/
|
*/
|
||||||
public class PageResult<T> {
|
public class PageResult<T> extends BasePageResult<T> {
|
||||||
|
|
||||||
/** 总数据量 */
|
|
||||||
protected long total;
|
|
||||||
|
|
||||||
/** 总页数 */
|
|
||||||
protected int pages;
|
|
||||||
|
|
||||||
protected List<T> list;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取总数据量
|
|
||||||
*
|
|
||||||
* @return 总数据量
|
|
||||||
*/
|
|
||||||
public long getTotal() {
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置总数据量
|
|
||||||
*
|
|
||||||
* @param total 总数据量
|
|
||||||
*/
|
|
||||||
public void setTotal(long total) {
|
|
||||||
this.total = total;
|
|
||||||
if (TimiJava.isNotEmpty(list)) {
|
|
||||||
pages = Calc.ceil(1D * total / list.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<T> getList() {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setList(List<T> list) {
|
|
||||||
this.list = list;
|
|
||||||
if (TimiJava.isNotEmpty(list)) {
|
|
||||||
pages = Calc.ceil(1D * total / list.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPages() {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/main/java/com/imyeyu/spring/bean/RequestRange.java
Normal file
35
src/main/java/com/imyeyu/spring/bean/RequestRange.java
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.imyeyu.spring.bean;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求范围参数
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2025-07-14 17:09
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RequestRange {
|
||||||
|
|
||||||
|
/** 起始值 */
|
||||||
|
private long start;
|
||||||
|
|
||||||
|
/** 结束值 */
|
||||||
|
private long end;
|
||||||
|
|
||||||
|
/** 总数据量 */
|
||||||
|
private long total;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取范围长度
|
||||||
|
*
|
||||||
|
* @return 范围长度
|
||||||
|
*/
|
||||||
|
public long getLength() {
|
||||||
|
return end - start + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.imyeyu.spring.config;
|
|||||||
|
|
||||||
import com.imyeyu.spring.bean.RedisConfigParams;
|
import com.imyeyu.spring.bean.RedisConfigParams;
|
||||||
import com.imyeyu.spring.util.Redis;
|
import com.imyeyu.spring.util.Redis;
|
||||||
|
import io.lettuce.core.api.StatefulConnection;
|
||||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||||
import org.springframework.cache.annotation.CachingConfigurer;
|
import org.springframework.cache.annotation.CachingConfigurer;
|
||||||
import org.springframework.cache.interceptor.KeyGenerator;
|
import org.springframework.cache.interceptor.KeyGenerator;
|
||||||
@@ -22,6 +23,12 @@ import java.time.Duration;
|
|||||||
*/
|
*/
|
||||||
public abstract class AbstractRedisConfig implements CachingConfigurer {
|
public abstract class AbstractRedisConfig implements CachingConfigurer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Redis 配置
|
||||||
|
*/
|
||||||
|
protected AbstractRedisConfig() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 Redis 基本配置
|
* 构建 Redis 基本配置
|
||||||
*
|
*
|
||||||
@@ -33,7 +40,7 @@ public abstract class AbstractRedisConfig implements CachingConfigurer {
|
|||||||
* 连接池配置
|
* 连接池配置
|
||||||
* <p>参考:
|
* <p>参考:
|
||||||
* <pre>
|
* <pre>
|
||||||
* GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
|
* GenericObjectPoolConfig<StatefulConnection<?, ?>> config = new GenericObjectPoolConfig<>();
|
||||||
* config.setMaxTotal(config.getMaxActive());
|
* config.setMaxTotal(config.getMaxActive());
|
||||||
* config.setMinIdle(config.getMinIdle());
|
* config.setMinIdle(config.getMinIdle());
|
||||||
* config.setMaxIdle(config.getMaxIdle());
|
* config.setMaxIdle(config.getMaxIdle());
|
||||||
@@ -42,7 +49,7 @@ public abstract class AbstractRedisConfig implements CachingConfigurer {
|
|||||||
*
|
*
|
||||||
* @return GenericObjectPoolConfig
|
* @return GenericObjectPoolConfig
|
||||||
*/
|
*/
|
||||||
public abstract GenericObjectPoolConfig<?> getPoolConfig();
|
public abstract GenericObjectPoolConfig<StatefulConnection<?, ?>> getPoolConfig();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redis key 生成策略
|
* Redis key 生成策略
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
47
src/main/java/com/imyeyu/spring/entity/Multilingual.java
Normal file
47
src/main/java/com/imyeyu/spring/entity/Multilingual.java
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package com.imyeyu.spring.entity;
|
||||||
|
|
||||||
|
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 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, Destroyable {
|
||||||
|
|
||||||
|
/** 唯一标识 */
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基本 SQL 映射,子接口可以不实现
|
* 基本 SQL 映射
|
||||||
*
|
*
|
||||||
|
* @param <T> 实体类型
|
||||||
|
* @param <P> 主键类型
|
||||||
* @author 夜雨
|
* @author 夜雨
|
||||||
* @version 2021-07-16 09:40
|
* @version 2021-07-16 09:40
|
||||||
*/
|
*/
|
||||||
@@ -25,37 +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 对象查询数据列表
|
||||||
*
|
*
|
||||||
* @return 数据量
|
* @param page 分页参数
|
||||||
*/
|
|
||||||
long count();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取部分数据
|
|
||||||
*
|
|
||||||
* @param offset 偏移
|
|
||||||
* @param limit 数据量
|
|
||||||
* @return 数据列表
|
* @return 数据列表
|
||||||
*/
|
*/
|
||||||
List<T> list(long offset, int limit);
|
@SelectProvider(type = SQLProvider.class, method = "selectByPage")
|
||||||
|
List<T> selectByPage(Page<T> page);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建数据。默认自增主键为 id,如需修改请重写此接口
|
* 根据 Page 对象统计数据量
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return 数据量
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = SQLProvider.class, method = "countByPage")
|
||||||
|
long countByPage(Page<T> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部数据
|
||||||
|
*
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = SQLProvider.class, method = "selectAll")
|
||||||
|
List<T> selectAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据
|
||||||
*
|
*
|
||||||
* @param t 数据对象
|
* @param t 数据对象
|
||||||
*/
|
*/
|
||||||
@@ -72,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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改数据
|
* 修改数据
|
||||||
@@ -86,6 +155,14 @@ 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")
|
||||||
|
void updateSelective(T t);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 软删除
|
* 软删除
|
||||||
*
|
*
|
||||||
@@ -94,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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁(物理删除)
|
* 销毁(物理删除)
|
||||||
*
|
*
|
||||||
@@ -101,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);
|
||||||
}
|
}
|
||||||
|
|||||||
209
src/main/java/com/imyeyu/spring/mapper/DynamicTableMapper.java
Normal file
209
src/main/java/com/imyeyu/spring/mapper/DynamicTableMapper.java
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package com.imyeyu.spring.mapper;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.bean.Logic;
|
||||||
|
import com.imyeyu.spring.bean.Page;
|
||||||
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
|
import com.imyeyu.spring.util.DynamicTableSQLProvider;
|
||||||
|
import org.apache.ibatis.annotations.DeleteProvider;
|
||||||
|
import org.apache.ibatis.annotations.InsertProvider;
|
||||||
|
import org.apache.ibatis.annotations.Options;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.SelectProvider;
|
||||||
|
import org.apache.ibatis.annotations.UpdateProvider;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持动态表名的 SQL 映射接口
|
||||||
|
* <p>相比 {@link BaseMapper},所有方法都需要显式传入表名参数</p>
|
||||||
|
*
|
||||||
|
* @param <T> 实体类型
|
||||||
|
* @param <P> 主键类型
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-01-07 11:00
|
||||||
|
*/
|
||||||
|
public interface DynamicTableMapper<T, P> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Page 对象查询数据列表
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectByPage")
|
||||||
|
List<T> selectByPage(@Param("tableName") String tableName, @Param("page") Page<T> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Page 对象统计数据量
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return 数据量
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "countByPage")
|
||||||
|
long countByPage(@Param("tableName") String tableName, @Param("page") Page<T> page);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return 分页结果
|
||||||
|
*/
|
||||||
|
default PageResult<T> selectPageResult(String tableName, Page<T> page) {
|
||||||
|
PageResult<T> result = new PageResult<>();
|
||||||
|
result.setTotal(countByPage(tableName, page));
|
||||||
|
result.setList(selectByPage(tableName, page));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectAll")
|
||||||
|
List<T> selectAll(@Param("tableName") String tableName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 数据对象
|
||||||
|
*/
|
||||||
|
@InsertProvider(type = DynamicTableSQLProvider.class, method = "insert")
|
||||||
|
@Options(useGeneratedKeys = true, keyProperty = "entity.id")
|
||||||
|
void insert(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 获取对象
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param id 索引
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "select")
|
||||||
|
T select(@Param("tableName") String tableName, @Param("id") P id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询单条数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
default T selectByExample(String tableName, T entity) {
|
||||||
|
return selectByExample(tableName, entity, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询单条数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectByExample")
|
||||||
|
T selectByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询全部数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
default List<T> selectAllByExample(String tableName, T entity) {
|
||||||
|
return selectAllByExample(tableName, entity, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询全部数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = DynamicTableSQLProvider.class, method = "selectAllByExample")
|
||||||
|
List<T> selectAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改数据
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 数据对象
|
||||||
|
*/
|
||||||
|
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "update")
|
||||||
|
void update(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择性更新
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 数据对象
|
||||||
|
*/
|
||||||
|
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "updateSelective")
|
||||||
|
void updateSelective(@Param("tableName") String tableName, @Param("entity") T entity);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param id 索引
|
||||||
|
*/
|
||||||
|
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "delete")
|
||||||
|
void delete(@Param("tableName") String tableName, @Param("id") P id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例批量逻辑删除
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
*/
|
||||||
|
default void deleteAllByExample(String tableName, T entity) {
|
||||||
|
deleteAllByExample(tableName, entity, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例批量逻辑删除
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
*/
|
||||||
|
@UpdateProvider(type = DynamicTableSQLProvider.class, method = "deleteAllByExample")
|
||||||
|
void deleteAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁(物理删除)
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param id 索引
|
||||||
|
*/
|
||||||
|
@DeleteProvider(type = DynamicTableSQLProvider.class, method = "destroy")
|
||||||
|
void destroy(@Param("tableName") String tableName, @Param("id") P id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例批量销毁(物理删除)
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
*/
|
||||||
|
default void destroyAllByExample(String tableName, T entity) {
|
||||||
|
destroyAllByExample(tableName, entity, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例批量销毁(物理删除)
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param entity 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
*/
|
||||||
|
@DeleteProvider(type = DynamicTableSQLProvider.class, method = "destroyAllByExample")
|
||||||
|
void destroyAllByExample(@Param("tableName") String tableName, @Param("entity") T entity, @Param("logic") Logic logic);
|
||||||
|
}
|
||||||
76
src/main/java/com/imyeyu/spring/mapper/RawMapper.java
Normal file
76
src/main/java/com/imyeyu/spring/mapper/RawMapper.java
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package com.imyeyu.spring.mapper;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.bean.Logic;
|
||||||
|
import com.imyeyu.spring.util.RawSQLProvider;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.SelectProvider;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始 SQL 映射
|
||||||
|
*
|
||||||
|
* @param <T> 实体类型
|
||||||
|
* @param <P> 主键类型
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-01-05 12:58
|
||||||
|
*/
|
||||||
|
public interface RawMapper<T, P> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部数据
|
||||||
|
*
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = RawSQLProvider.class, method = "selectAll")
|
||||||
|
List<T> selectAllRaw();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 获取对象
|
||||||
|
*
|
||||||
|
* @param id 索引
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = RawSQLProvider.class, method = "select")
|
||||||
|
T selectRaw(P id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询单条数据
|
||||||
|
*
|
||||||
|
* @param t 示例对象
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
default T selectByExampleRaw(T t) {
|
||||||
|
return selectByExampleRaw(t, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询单条数据
|
||||||
|
*
|
||||||
|
* @param t 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
* @return 数据对象
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = RawSQLProvider.class, method = "selectByExample")
|
||||||
|
T selectByExampleRaw(@Param("entity") T t, @Param("logic") Logic logic);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询全部数据
|
||||||
|
*
|
||||||
|
* @param t 示例对象
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
default List<T> selectAllByExampleRaw(T t) {
|
||||||
|
return selectAllByExampleRaw(t, Logic.AND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据示例查询全部数据
|
||||||
|
*
|
||||||
|
* @param t 示例对象
|
||||||
|
* @param logic 条件连接逻辑
|
||||||
|
* @return 数据列表
|
||||||
|
*/
|
||||||
|
@SelectProvider(type = RawSQLProvider.class, method = "selectAllByExample")
|
||||||
|
List<T> selectAllByExampleRaw(@Param("entity") T t, @Param("logic") Logic logic);
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package com.imyeyu.spring.service;
|
|||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +59,7 @@ public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
|
|||||||
|
|
||||||
public void update(T t) {
|
public void update(T t) {
|
||||||
checkMapper();
|
checkMapper();
|
||||||
baseMapper.update(t);
|
baseMapper.updateSelective(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(P p) {
|
public void delete(P p) {
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
715
src/main/java/com/imyeyu/spring/util/BaseSQLProvider.java
Normal file
715
src/main/java/com/imyeyu/spring/util/BaseSQLProvider.java
Normal 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<列名,字段名> */
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.imyeyu.spring.util;
|
package com.imyeyu.spring.util;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.validation.ValidationException;
|
|
||||||
import com.imyeyu.java.bean.timi.TimiCode;
|
import com.imyeyu.java.bean.timi.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.bean.timi.TimiResponse;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.TypeMismatchException;
|
import org.springframework.beans.TypeMismatchException;
|
||||||
@@ -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) {
|
||||||
@@ -41,7 +48,7 @@ public class GlobalExceptionHandler {
|
|||||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||||
log.error("conversion error", e);
|
log.error("conversion error", e);
|
||||||
}
|
}
|
||||||
return new TimiResponse<>(TimiCode.ARG_BAD);
|
return new TimiResponse<>(TimiCode.ARG_BAD).msgKey("invalid.body");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,7 +63,7 @@ public class GlobalExceptionHandler {
|
|||||||
if (env.contains("dev") || log.isDebugEnabled()) {
|
if (env.contains("dev") || log.isDebugEnabled()) {
|
||||||
log.error("header error", e);
|
log.error("header error", e);
|
||||||
}
|
}
|
||||||
return new TimiResponse<>(TimiCode.REQUEST_BAD);
|
return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.request");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,13 +78,13 @@ public class GlobalExceptionHandler {
|
|||||||
log.warn("request error", e);
|
log.warn("request error", e);
|
||||||
FieldError error = subE.getBindingResult().getFieldError();
|
FieldError error = subE.getBindingResult().getFieldError();
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return new TimiResponse<>(TimiCode.ARG_BAD, error.getDefaultMessage());
|
return new TimiResponse<>(TimiCode.ARG_BAD, "[%s] %s".formatted(error.getField(), error.getDefaultMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
||||||
log.error("request error", e);
|
log.error("request error", e);
|
||||||
}
|
}
|
||||||
return new TimiResponse<>(TimiCode.REQUEST_BAD);
|
return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.arg");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,8 +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) {
|
||||||
// TODO 400 以下即使是开发环境也不算异常
|
if (!env.startsWith("prod") || log.isDebugEnabled()) {
|
||||||
if (env.startsWith("dev") || log.isDebugEnabled()) {
|
|
||||||
log.error(timiE.getMessage(), e);
|
log.error(timiE.getMessage(), e);
|
||||||
}
|
}
|
||||||
// 一般异常
|
// 一般异常
|
||||||
@@ -98,6 +104,6 @@ public class GlobalExceptionHandler {
|
|||||||
}
|
}
|
||||||
// 致命异常
|
// 致命异常
|
||||||
log.error("fatal error", e);
|
log.error("fatal error", e);
|
||||||
return new TimiResponse<>(TimiCode.ERROR);
|
return new TimiResponse<>(TimiCode.ERROR).msgKey("service.error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package com.imyeyu.spring.util;
|
|||||||
|
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||||
|
import com.imyeyu.java.bean.LanguageMsgMapping;
|
||||||
import com.imyeyu.java.bean.timi.TimiCode;
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
import com.imyeyu.java.bean.timi.TimiResponse;
|
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;
|
||||||
@@ -26,12 +28,20 @@ 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<String, 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) {
|
||||||
@@ -54,9 +64,14 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
|||||||
} else {
|
} else {
|
||||||
result = new TimiResponse<>(TimiCode.SUCCESS, body);
|
result = new TimiResponse<>(TimiCode.SUCCESS, body);
|
||||||
}
|
}
|
||||||
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
|
try {
|
||||||
result.setMsg(multilingualHeader.handler(result.getMsgKey()));
|
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
|
||||||
} else if (TimiJava.isEmpty(result.getMsg())) {
|
result.setMsg(multilingualHeader.handler(result));
|
||||||
|
} else if (TimiJava.isEmpty(result.getMsg())) {
|
||||||
|
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("multilingual response error", e);
|
||||||
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
|
||||||
}
|
}
|
||||||
if (30000 < result.getCode()) {
|
if (30000 < result.getCode()) {
|
||||||
@@ -64,12 +79,4 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallbackArgReturn<String, String> getMultilingualHeader() {
|
|
||||||
return multilingualHeader;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMultilingualHeader(CallbackArgReturn<String, String> multilingualHeader) {
|
|
||||||
this.multilingualHeader = multilingualHeader;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/main/java/com/imyeyu/spring/util/RawSQLProvider.java
Normal file
46
src/main/java/com/imyeyu/spring/util/RawSQLProvider.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.imyeyu.spring.util;
|
||||||
|
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import com.imyeyu.spring.bean.Logic;
|
||||||
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.builder.annotation.ProviderContext;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原始 Mapper SQL 代理器
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-01-05 13:00
|
||||||
|
*/
|
||||||
|
public class RawSQLProvider extends SQLProvider {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String selectAll(ProviderContext context) {
|
||||||
|
EntityMeta meta = getEntityMeta(context);
|
||||||
|
return "SELECT * FROM %s WHERE 1 = 1".formatted(meta.table);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String select(ProviderContext context, Object id) {
|
||||||
|
EntityMeta meta = getEntityMeta(context);
|
||||||
|
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
|
||||||
|
return "SELECT %s FROM `%s` WHERE `%s` = #{%s}".formatted(meta.selectAllClause, meta.table, meta.idFieldColumn.columnName, id) + " LIMIT 1";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String selectByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||||
|
return selectAllByExample(entity, logic) + BaseMapper.LIMIT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String selectAllByExample(@Param("entity") Object entity, @Param("logic") Logic logic) {
|
||||||
|
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||||
|
String conditionClause = meta.fieldColumnList.stream()
|
||||||
|
.filter(fc -> fc.isNotEmpty(entity))
|
||||||
|
.map(fc -> "`%s` = #{entity.%s}".formatted(fc.columnName, fc.fieldName))
|
||||||
|
.collect(Collectors.joining(" %s ".formatted(logic)));
|
||||||
|
return "SELECT %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, meta.table, conditionClause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
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 lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
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 +25,25 @@ 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
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class Redis<K, V> {
|
public class Redis<K, V> {
|
||||||
|
|
||||||
private final RedisSerializer<K> serializer;
|
private RedisTemplate<K, V> redis;
|
||||||
private final RedisTemplate<K, V> redis;
|
private RedisSerializer<K> serializer;
|
||||||
|
|
||||||
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 +51,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 +87,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 +196,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 +221,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 +236,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 +266,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,35 @@ public class RedisSerializers {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Gson 序列化 */
|
/**
|
||||||
public static <T> RedisSerializer<T> gsonSerializer(Class<T> clazz) {
|
* Json 序列化
|
||||||
return new RedisSerializer<>() {
|
*
|
||||||
|
* @param <T> 数据类型
|
||||||
|
* @param clazz 数据类型
|
||||||
|
* @return Redis 序列化器
|
||||||
|
*/
|
||||||
|
public static <T> RedisSerializer<T> jacksonSerializer(Class<T> clazz) {
|
||||||
|
return jacksonSerializer(new ObjectMapper(), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
private static final Gson GSON = new Gson();
|
/**
|
||||||
|
* Json 序列化
|
||||||
|
*
|
||||||
|
* @param <T> 数据类型
|
||||||
|
* @param mapper 序列化对象
|
||||||
|
* @param clazz 数据类型
|
||||||
|
* @return Redis 序列化器
|
||||||
|
*/
|
||||||
|
public static <T> RedisSerializer<T> jacksonSerializer(ObjectMapper mapper, Class<T> clazz) {
|
||||||
|
return new RedisSerializer<>() {
|
||||||
|
|
||||||
@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 mapper.writeValueAsString(object).getBytes(StandardCharsets.UTF_8);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,7 +120,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 mapper.readValue(new String(bytes, StandardCharsets.UTF_8), clazz);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,77 @@
|
|||||||
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.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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Page 对象查询数据列表
|
||||||
|
*
|
||||||
|
* @param context 代理器上下文
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return SQL
|
||||||
|
*/
|
||||||
|
public String selectByPage(ProviderContext context, @Param("page") Page<?> page) {
|
||||||
|
EntityMeta meta = getEntityMeta(context);
|
||||||
|
return buildSelectByPageSQL(meta, meta.table, page, "#{offset}", "#{limit}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Page 对象统计数据量
|
||||||
|
*
|
||||||
|
* @param context 代理器上下文
|
||||||
|
* @param page 分页参数
|
||||||
|
* @return SQL
|
||||||
|
*/
|
||||||
|
public String countByPage(ProviderContext context, @Param("page") Page<?> page) {
|
||||||
|
EntityMeta meta = getEntityMeta(context);
|
||||||
|
return buildCountByPageSQL(meta, meta.table, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部数据
|
||||||
|
*
|
||||||
|
* @param context 代理器上下文
|
||||||
|
* @return SQL
|
||||||
|
*/
|
||||||
|
public String selectAll(ProviderContext context) {
|
||||||
|
EntityMeta meta = getEntityMeta(context);
|
||||||
|
StringBuilder sql = new StringBuilder();
|
||||||
|
sql.append("SELECT * FROM %s WHERE 1 = 1".formatted(meta.table));
|
||||||
|
if (meta.canDelete) {
|
||||||
|
sql.append(BaseMapper.NOT_DELETE);
|
||||||
|
}
|
||||||
|
return sql.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插入
|
* 插入
|
||||||
* <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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,83 +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 * FROM `%s` WHERE `%s` = #{%s}".formatted(meta.table, meta.idFieldColumn.columnName, id));
|
|
||||||
if (meta.canDelete) {
|
|
||||||
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
|
||||||
}
|
|
||||||
return sql.append(" LIMIT 1").toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据实体非空字段使用等号查询
|
* 根据实体非空字段使用等号查询
|
||||||
*
|
*
|
||||||
* @param entity 实体
|
* @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 * FROM `%s` WHERE %s".formatted(meta.table, conditionClause));
|
|
||||||
if (meta.canDelete) {
|
|
||||||
if (TimiJava.isNotEmpty(conditionClause)) {
|
|
||||||
sql.append(" AND ");
|
|
||||||
}
|
|
||||||
sql.append("(`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
|
|
||||||
}
|
|
||||||
return sql.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 ID 更新,需要实体实现 {@link Updatable}
|
* 根据 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 Deletable}
|
* 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link com.imyeyu.spring.entity.Updatable Updatable}
|
||||||
|
*
|
||||||
|
* @param entity 实体
|
||||||
|
* @return SQL
|
||||||
|
*/
|
||||||
|
public String updateSelective(Object entity) {
|
||||||
|
EntityMeta meta = getEntityMeta(entity.getClass());
|
||||||
|
return buildUpdateSelectiveSQL(meta, meta.table, entity, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 软删除,需要实体实现 {@link com.imyeyu.spring.entity.Deletable Deletable}
|
||||||
*
|
*
|
||||||
* @param context 代理器上下文
|
* @param context 代理器上下文
|
||||||
* @param id ID
|
* @param id ID
|
||||||
@@ -163,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
|
||||||
@@ -178,145 +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
|
||||||
*/
|
*/
|
||||||
private 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 元数据
|
|
||||||
*/
|
|
||||||
private EntityMeta getEntityMeta(Class<?> entityClass) {
|
|
||||||
return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实体元数据
|
|
||||||
*
|
|
||||||
* @author 夜雨
|
|
||||||
* @since 2025-02-05 23:47
|
|
||||||
*/
|
|
||||||
private static class EntityMeta {
|
|
||||||
|
|
||||||
/** 实体类 */
|
|
||||||
final Class<?> entityClass;
|
|
||||||
|
|
||||||
/** 表名 */
|
|
||||||
final String table;
|
|
||||||
|
|
||||||
/** ID 字段 */
|
|
||||||
final FieldColumn idFieldColumn;
|
|
||||||
|
|
||||||
/** 只读的列名字段名映射,Map<列名,字段名> */
|
|
||||||
final List<FieldColumn> fieldColumnList;
|
|
||||||
|
|
||||||
/** true 为可更新 */
|
|
||||||
final boolean canUpdate;
|
|
||||||
|
|
||||||
/** true 为可删除(软删除) */
|
|
||||||
final boolean canDelete;
|
|
||||||
|
|
||||||
/** true 为可销毁(硬删除) */
|
|
||||||
final boolean canDestroy;
|
|
||||||
|
|
||||||
public EntityMeta(Class<?> entityClass) {
|
|
||||||
this.entityClass = entityClass;
|
|
||||||
|
|
||||||
// 表名
|
|
||||||
while (entityClass.isAnnotationPresent(Transient.class)) {
|
|
||||||
entityClass = entityClass.getSuperclass();
|
|
||||||
}
|
|
||||||
Table table = entityClass.getAnnotation(Table.class);
|
|
||||||
if (table == null) {
|
|
||||||
this.table = Text.camelCase2underscore(entityClass.getSimpleName());
|
|
||||||
} else {
|
|
||||||
this.table = table.value();
|
|
||||||
TimiException.required(this.table, String.format("empty table annotation value for %s entity", entityClass.getName()));
|
|
||||||
}
|
|
||||||
List<Field> allFieldList = Ref.listAllFields(entityClass);
|
|
||||||
|
|
||||||
FieldColumn idFieldColumn = null;
|
|
||||||
List<FieldColumn> fieldColumnList = new ArrayList<>();
|
|
||||||
for (int i = 0; i < allFieldList.size(); i++) {
|
|
||||||
Field field = allFieldList.get(i);
|
|
||||||
if (field.isAnnotationPresent(Transient.class)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
FieldColumn fieldColumn = new FieldColumn(field);
|
|
||||||
if (fieldColumn.isId) {
|
|
||||||
TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName()));
|
|
||||||
idFieldColumn = fieldColumn;
|
|
||||||
}
|
|
||||||
fieldColumnList.add(fieldColumn);
|
|
||||||
}
|
|
||||||
this.idFieldColumn = idFieldColumn;
|
|
||||||
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
|
|
||||||
canUpdate = Updatable.class.isAssignableFrom(entityClass);
|
|
||||||
canDelete = Deletable.class.isAssignableFrom(entityClass);
|
|
||||||
canDestroy = Destroyable.class.isAssignableFrom(entityClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实体字段属性
|
|
||||||
*
|
|
||||||
* @author 夜雨
|
|
||||||
* @since 2025-02-07 09:54
|
|
||||||
*/
|
|
||||||
private static class FieldColumn {
|
|
||||||
|
|
||||||
/** 字段 */
|
|
||||||
final Field field;
|
|
||||||
|
|
||||||
/** 字段名 */
|
|
||||||
final String fieldName;
|
|
||||||
|
|
||||||
/** 列名 */
|
|
||||||
final String columnName;
|
|
||||||
|
|
||||||
/** true 为 ID */
|
|
||||||
final boolean isId;
|
|
||||||
|
|
||||||
/** true 为自动生成 UUID */
|
|
||||||
final boolean isAutoUUID;
|
|
||||||
|
|
||||||
final boolean isAutoUpperUUID;
|
|
||||||
|
|
||||||
public FieldColumn(Field field) {
|
|
||||||
this.field = field;
|
|
||||||
|
|
||||||
fieldName = field.getName();
|
|
||||||
Column column = field.getAnnotation(Column.class);
|
|
||||||
if (column == null) {
|
|
||||||
columnName = Text.camelCase2underscore(field.getName());
|
|
||||||
} else {
|
|
||||||
columnName = column.value();
|
|
||||||
TimiException.required(columnName, "empty field:%s column annotation value for %s entity".formatted(field.getName(), field.getDeclaringClass()));
|
|
||||||
}
|
|
||||||
isId = field.isAnnotationPresent(Id.class);
|
|
||||||
isAutoUUID = field.isAnnotationPresent(AutoUUID.class);
|
|
||||||
if (isAutoUUID) {
|
|
||||||
isAutoUpperUUID = field.getAnnotation(AutoUUID.class).upper();
|
|
||||||
} else {
|
|
||||||
isAutoUpperUUID = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.imyeyu.spring.util;
|
||||||
|
|
||||||
|
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
import org.springframework.core.io.support.PropertySourceFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yaml 属性源加载工厂
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2025-10-13 16:29
|
||||||
|
*/
|
||||||
|
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Yaml 属性源工厂
|
||||||
|
*/
|
||||||
|
public YamlPropertySourceFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @org.springframework.lang.NonNull PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
|
||||||
|
List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
|
||||||
|
return sources.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/main/resources/lang/common.lang
Normal file
5
src/main/resources/lang/common.lang
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
invalid.arg=无效的参数
|
||||||
|
invalid.body=无效的请求体
|
||||||
|
invalid.header=无效的请求头
|
||||||
|
invalid.request=无效的请求
|
||||||
|
service.error=服务错误
|
||||||
Reference in New Issue
Block a user