feat: initialize project
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
logs/
|
||||
log/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*.bak
|
||||
*.orig
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
397
docs/API_EXAMPLES.md
Normal file
397
docs/API_EXAMPLES.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# 接口联调示例
|
||||
|
||||
## 启动顺序
|
||||
|
||||
1. 执行 `src/main/resources/sql/init-auth-schema.sql`
|
||||
2. 执行 `src/main/resources/sql/init-demo-data.sql`
|
||||
3. 启动应用
|
||||
4. 访问 `POST /api/captcha/generate`
|
||||
5. 使用返回的验证码登录
|
||||
|
||||
## 1. 获取验证码
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/captcha/generate
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"key": "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d",
|
||||
"image": "data:image/png;base64,..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 登录
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123",
|
||||
"captchaKey": "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d",
|
||||
"captchaCode": "a8k3"
|
||||
}
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": {
|
||||
"token": "xxxxxx",
|
||||
"tokenName": "Authorization",
|
||||
"userInfo": {
|
||||
"id": 1000000000000000001,
|
||||
"username": "admin",
|
||||
"nickname": "系统管理员",
|
||||
"status": 1,
|
||||
"statusDesc": "正常"
|
||||
},
|
||||
"roles": ["ADMIN"],
|
||||
"permissions": [
|
||||
"system:user:page",
|
||||
"system:role:page",
|
||||
"system:menu:list"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后续请求头:
|
||||
|
||||
```http
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 3. 查询当前用户菜单
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/menu/current
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 4. 查询当前用户权限标识
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/menu/permissions/current
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 5. 查询前端下拉字典
|
||||
|
||||
查询字典类型:
|
||||
|
||||
```http
|
||||
POST /api/enums/list
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"enumName": "LogType",
|
||||
"enumDesc": "日志类型枚举",
|
||||
"displayName": "LogType",
|
||||
"className": "com.test.demo.enums.LogType",
|
||||
"packageName": "com.test.demo.enums",
|
||||
"valueType": "java.lang.String",
|
||||
"sourceType": "SYSTEM",
|
||||
"builtin": 1,
|
||||
"queryPath": "/api/enums/LogType"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
查询字典项:
|
||||
|
||||
```http
|
||||
POST /api/enums/items/LogType
|
||||
```
|
||||
|
||||
返回示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"value": "LOGIN",
|
||||
"desc": "登录日志"
|
||||
},
|
||||
{
|
||||
"value": "OPER",
|
||||
"desc": "操作日志"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 新增自定义字典类型
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/enum-manage/type/add
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"enumCode": "PayChannel",
|
||||
"enumName": "支付渠道",
|
||||
"enumDesc": "支付渠道字典",
|
||||
"enabled": 1,
|
||||
"sort": 1,
|
||||
"remark": "联调新增"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 新增自定义字典项
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/enum-manage/item/add
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"enumCode": "PayChannel",
|
||||
"itemValue": "ALIPAY",
|
||||
"itemLabel": "支付宝",
|
||||
"itemName": "ALIPAY",
|
||||
"enabled": 1,
|
||||
"sort": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 启停字典类型
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/enum-manage/type/enable/2033103240660238338?enabled=0
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 9. 启停字典项
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/enum-manage/item/enable/2033103240752513025?enabled=1
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 10. 查询登录日志
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/log/login/page
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"pageNum": 1,
|
||||
"pageSize": 10,
|
||||
"username": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
## 11. 导出登录日志
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/log/login/export
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"logType": "LOGIN"
|
||||
}
|
||||
```
|
||||
|
||||
## 12. 查询操作日志
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/log/oper/page
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"pageNum": 1,
|
||||
"pageSize": 10,
|
||||
"userName": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
## 13. 导出操作日志
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/log/oper/export
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"userName": "admin",
|
||||
"logType": "OPER"
|
||||
}
|
||||
```
|
||||
|
||||
## 14. 查询文件列表
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/file/list
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"relativeDir": "",
|
||||
"recursive": true,
|
||||
"pageNum": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
```
|
||||
|
||||
## 15. 上传文件
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/file/upload
|
||||
Authorization: xxxxxx
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
`file`: 文件
|
||||
|
||||
## 16. 下载文件
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
GET /api/file/download/2026/03/15/demo.xlsx
|
||||
Authorization: xxxxxx
|
||||
```
|
||||
|
||||
## 17. 预览文件
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
GET /api/file/preview/2026/03/15/demo.xlsx
|
||||
```
|
||||
|
||||
## 18. 导入用户
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/user/import
|
||||
Authorization: xxxxxx
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
参数:
|
||||
|
||||
`file`: 用户导入模板
|
||||
|
||||
## 19. 导出用户
|
||||
|
||||
接口:
|
||||
|
||||
```http
|
||||
POST /api/user/export
|
||||
Authorization: xxxxxx
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"pageNum": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
```
|
||||
|
||||
## 20. 可选签名请求头
|
||||
|
||||
如果客户端要启用签名,可附带以下请求头:
|
||||
|
||||
```http
|
||||
X-Timestamp: 1710000000
|
||||
X-Nonce: random-string
|
||||
X-Sign: sha256(timestamp + nonce + body + signSecret)
|
||||
```
|
||||
|
||||
签名密钥配置在:
|
||||
|
||||
`security.sign.secret`
|
||||
295
mvnw
vendored
Normal file
295
mvnw
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
mvnw.cmd
vendored
Normal file
189
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
240
pom.xml
Normal file
240
pom.xml
Normal file
@@ -0,0 +1,240 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.7</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.test</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>demo</name>
|
||||
<description>Spring Boot 4 后台管理脚手架</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<mybatis-plus.version>3.5.10.1</mybatis-plus.version>
|
||||
<sa-token.version>1.43.0</sa-token.version>
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<springdoc.version>2.8.6</springdoc.version>
|
||||
<redisson.version>3.40.2</redisson.version>
|
||||
<hutool.version>5.8.43</hutool.version>
|
||||
<bouncycastle.version>1.83</bouncycastle.version>
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<easy-captcha.version>1.6.2</easy-captcha.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web (Servlet) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Undertow 容器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redisson 分布式锁 -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Cache (Caffeine) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<!-- MyBatis-Plus JSqlParser(分页/防攻击插件) -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Sa-Token 核心 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 整合 Redis (Jackson序列化) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j (OpenAPI3 Jakarta) -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
<!-- Hutool 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- BouncyCastle 国密SM3/SM4 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- FastExcel 导入导出 -->
|
||||
<dependency>
|
||||
<groupId>cn.idev.excel</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>${fastexcel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- easy-captcha 验证码 -->
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
<version>${easy-captcha.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct 对象转换 -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 邮件服务 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Actuator 监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson Datatype JSR310 (LocalDateTime) -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Pool2 (Redis连接池) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>${lombok-mapstruct-binding.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
13
src/main/java/com/test/demo/DemoApplication.java
Normal file
13
src/main/java/com/test/demo/DemoApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DemoApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
165
src/main/java/com/test/demo/aspect/LogAspect.java
Normal file
165
src/main/java/com/test/demo/aspect/LogAspect.java
Normal file
@@ -0,0 +1,165 @@
|
||||
package com.test.demo.aspect;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.test.demo.bo.LoginBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.log.event.OperLogEvent;
|
||||
import com.test.demo.security.SecurityUtil;
|
||||
import com.test.demo.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作日志切面
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final UserService userService;
|
||||
|
||||
@Around("@annotation(logAnnotation)")
|
||||
public Object around(ProceedingJoinPoint point, Log logAnnotation) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
OperLogEntity operLog = new OperLogEntity();
|
||||
operLog.setTitle(logAnnotation.value());
|
||||
operLog.setLogType(logAnnotation.type().name());
|
||||
|
||||
// 请求信息
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
operLog.setRequestUrl(request.getRequestURI());
|
||||
operLog.setRequestMethod(request.getMethod());
|
||||
operLog.setIp(getClientIp(request));
|
||||
}
|
||||
|
||||
// 方法信息
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
operLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getName());
|
||||
|
||||
// 请求参数(脱敏)
|
||||
try {
|
||||
String params = objectMapper.writeValueAsString(point.getArgs());
|
||||
operLog.setRequestParam(truncate(params, 2000));
|
||||
} catch (Exception e) {
|
||||
operLog.setRequestParam("参数序列化失败");
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
try {
|
||||
Long userId = SecurityUtil.getUserIdOrNull();
|
||||
operLog.setUserId(userId);
|
||||
if (userId != null) {
|
||||
UserEntity user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
operLog.setUserName(user.getUsername());
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
Object result = null;
|
||||
try {
|
||||
result = point.proceed();
|
||||
operLog.setStatus(1);
|
||||
// 返回结果(脱敏)
|
||||
try {
|
||||
String resultStr = objectMapper.writeValueAsString(result);
|
||||
operLog.setResponseResult(truncate(resultStr, 2000));
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Throwable e) {
|
||||
operLog.setStatus(0);
|
||||
operLog.setErrorMsg(truncate(e.getMessage(), 2000));
|
||||
throw e;
|
||||
} finally {
|
||||
if (operLog.getUserId() == null) {
|
||||
Long userId = SecurityUtil.getUserIdOrNull();
|
||||
if (userId != null) {
|
||||
operLog.setUserId(userId);
|
||||
UserEntity user = userService.getById(userId);
|
||||
if (user != null) {
|
||||
operLog.setUserName(user.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operLog.getUserName() == null) {
|
||||
operLog.setUserName(extractUsername(point.getArgs()));
|
||||
}
|
||||
operLog.setCostTime(System.currentTimeMillis() - startTime);
|
||||
operLog.setCreateTime(LocalDateTime.now());
|
||||
|
||||
// 日志输出
|
||||
switch (logAnnotation.level()) {
|
||||
case DEBUG -> log.debug("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
case WARN -> log.warn("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
case ERROR -> log.error("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
default -> log.info("[{}] {} 耗时:{}ms", logAnnotation.type(), logAnnotation.value(), operLog.getCostTime());
|
||||
}
|
||||
|
||||
// 异步写入数据库
|
||||
if (logAnnotation.saveToDB()) {
|
||||
eventPublisher.publishEvent(new OperLogEvent(this, operLog));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// 多级代理取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
private String truncate(String str, int maxLen) {
|
||||
if (str == null) return null;
|
||||
return str.length() > maxLen ? str.substring(0, maxLen) : str;
|
||||
}
|
||||
|
||||
private String extractUsername(Object[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof LoginBo loginBo) {
|
||||
return loginBo.getUsername();
|
||||
}
|
||||
if (arg == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
var method = arg.getClass().getMethod("getUsername");
|
||||
Object username = method.invoke(arg);
|
||||
if (username != null) {
|
||||
return String.valueOf(username);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/test/demo/aspect/RateLimitAspect.java
Normal file
77
src/main/java/com/test/demo/aspect/RateLimitAspect.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.test.demo.aspect;
|
||||
|
||||
import com.test.demo.common.annotation.RateLimit;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 限流切面(Redis 滑动窗口)
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String LUA_SCRIPT = """
|
||||
local key = KEYS[1]
|
||||
local limit = tonumber(ARGV[1])
|
||||
local window = tonumber(ARGV[2])
|
||||
local now = tonumber(ARGV[3])
|
||||
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
|
||||
local count = redis.call('ZCARD', key)
|
||||
if count < limit then
|
||||
redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
|
||||
redis.call('EXPIRE', key, window)
|
||||
return 1
|
||||
end
|
||||
return 0
|
||||
""";
|
||||
|
||||
@Around("@annotation(rateLimit)")
|
||||
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
|
||||
String key = buildKey(point, rateLimit);
|
||||
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
|
||||
List<String> keys = Collections.singletonList(key);
|
||||
Long result = redisTemplate.execute(script, keys,
|
||||
(long) rateLimit.count(), (long) rateLimit.time(), System.currentTimeMillis());
|
||||
|
||||
if (result == null || result == 0L) {
|
||||
log.warn("限流触发: key={}", key);
|
||||
throw new BizException(ResultCode.RATE_LIMIT);
|
||||
}
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
private String buildKey(ProceedingJoinPoint point, RateLimit rateLimit) {
|
||||
if (!rateLimit.key().isEmpty()) {
|
||||
return RedisKeyConstant.RATE_LIMIT + rateLimit.key();
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
|
||||
String ip = "";
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
ip = attributes.getRequest().getRemoteAddr();
|
||||
}
|
||||
return RedisKeyConstant.RATE_LIMIT + ip + ":" + methodName;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/test/demo/bo/EnumItemBo.java
Normal file
49
src/main/java/com/test/demo/bo/EnumItemBo.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 枚举/字典项请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "EnumItemBo", description = "枚举/字典项请求参数")
|
||||
public class EnumItemBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "字典项ID不能为空")
|
||||
@Schema(description = "字典项ID", example = "2033103240752513025")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "枚举编码不能为空")
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "字典值不能为空")
|
||||
@Schema(description = "字典值", example = "LOGIN")
|
||||
private String itemValue;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "显示文本不能为空")
|
||||
@Schema(description = "显示文本", example = "登录日志")
|
||||
private String itemLabel;
|
||||
|
||||
@Schema(description = "系统枚举常量名", example = "LOGIN")
|
||||
private String itemName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "标签颜色", example = "#409EFF")
|
||||
private String color;
|
||||
|
||||
@Schema(description = "标签类型", example = "success")
|
||||
private String tagType;
|
||||
|
||||
@Schema(description = "备注", example = "系统默认日志类型")
|
||||
private String remark;
|
||||
}
|
||||
41
src/main/java/com/test/demo/bo/EnumTypeBo.java
Normal file
41
src/main/java/com/test/demo/bo/EnumTypeBo.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "EnumTypeBo", description = "枚举/字典类型请求参数")
|
||||
public class EnumTypeBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "类型ID不能为空")
|
||||
@Schema(description = "类型ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "枚举编码不能为空")
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称", example = "日志类型")
|
||||
private String enumName;
|
||||
|
||||
@Schema(description = "枚举描述", example = "日志类型枚举")
|
||||
private String enumDesc;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许系统同步更新", example = "1")
|
||||
private Integer syncEnabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "系统内置日志类型")
|
||||
private String remark;
|
||||
}
|
||||
27
src/main/java/com/test/demo/bo/EnumTypeQueryBo.java
Normal file
27
src/main/java/com/test/demo/bo/EnumTypeQueryBo.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "EnumTypeQueryBo", description = "枚举/字典类型查询参数")
|
||||
public class EnumTypeQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称/描述关键字", example = "日志")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
}
|
||||
21
src/main/java/com/test/demo/bo/FileQueryBo.java
Normal file
21
src/main/java/com/test/demo/bo/FileQueryBo.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 文件查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "FileQueryBo", description = "文件查询参数")
|
||||
public class FileQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "相对目录", example = "2026/03/15")
|
||||
private String relativeDir;
|
||||
|
||||
@Schema(description = "是否递归查询子目录", example = "false", defaultValue = "false")
|
||||
private Boolean recursive = false;
|
||||
}
|
||||
29
src/main/java/com/test/demo/bo/LoginBo.java
Normal file
29
src/main/java/com/test/demo/bo/LoginBo.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "LoginBo", description = "登录请求参数")
|
||||
public class LoginBo {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "admin123")
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "验证码Key不能为空")
|
||||
@Schema(description = "验证码Key", example = "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d")
|
||||
private String captchaKey;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Schema(description = "验证码", example = "a8k3")
|
||||
private String captchaCode;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/LoginLogQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/LoginLogQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 登录日志查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "LoginLogQueryBo", description = "登录日志查询参数")
|
||||
public class LoginLogQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "日志类型", example = "LOGIN", allowableValues = {"LOGIN", "LOGOUT"})
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
54
src/main/java/com/test/demo/bo/MenuBo.java
Normal file
54
src/main/java/com/test/demo/bo/MenuBo.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 菜单请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "MenuBo", description = "菜单请求参数")
|
||||
public class MenuBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "菜单ID不能为空")
|
||||
@Schema(description = "菜单ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "父级ID", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "菜单类型不能为空")
|
||||
@Schema(description = "菜单类型", example = "MENU", allowableValues = {"MENU", "BUTTON"})
|
||||
private String menuType;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "菜单名称不能为空")
|
||||
@Schema(description = "菜单名称", example = "用户管理")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "路由路径", example = "/user")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "组件路径", example = "system/user/index")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "权限标识", example = "system:user:page")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "图标", example = "User")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "是否显示", example = "1")
|
||||
private Integer visible;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
27
src/main/java/com/test/demo/bo/OperLogQueryBo.java
Normal file
27
src/main/java/com/test/demo/bo/OperLogQueryBo.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 操作日志查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "OperLogQueryBo", description = "操作日志查询参数")
|
||||
public class OperLogQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "操作说明", example = "新增用户")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "日志类型", example = "OPER")
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "操作用户名", example = "admin")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
43
src/main/java/com/test/demo/bo/RegisterBo.java
Normal file
43
src/main/java/com/test/demo/bo/RegisterBo.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 注册请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RegisterBo", description = "注册请求参数")
|
||||
public class RegisterBo {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "zhangsan")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "123456")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "昵称", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Schema(description = "邮箱", example = "zhangsan@example.com")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像路径", example = "/avatar/default.png")
|
||||
private String avatar;
|
||||
|
||||
@NotBlank(message = "验证码Key不能为空")
|
||||
@Schema(description = "验证码Key", example = "8f3b8fd4d2cf4d1ca6a4b8cf7ab33f9d")
|
||||
private String captchaKey;
|
||||
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
@Schema(description = "验证码", example = "a8k3")
|
||||
private String captchaCode;
|
||||
}
|
||||
36
src/main/java/com/test/demo/bo/RoleBo.java
Normal file
36
src/main/java/com/test/demo/bo/RoleBo.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 角色请求参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RoleBo", description = "角色请求参数")
|
||||
public class RoleBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "角色ID不能为空")
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "角色编码不能为空")
|
||||
@Schema(description = "角色编码", example = "ADMIN")
|
||||
private String roleCode;
|
||||
|
||||
@NotBlank(groups = {ValidGroup.Add.class, ValidGroup.Update.class}, message = "角色名称不能为空")
|
||||
@Schema(description = "角色名称", example = "系统管理员")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
22
src/main/java/com/test/demo/bo/RoleMenuAssignBo.java
Normal file
22
src/main/java/com/test/demo/bo/RoleMenuAssignBo.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色菜单分配参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "RoleMenuAssignBo", description = "角色菜单分配参数")
|
||||
public class RoleMenuAssignBo {
|
||||
|
||||
@NotNull(message = "角色ID不能为空")
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "菜单ID列表")
|
||||
private List<Long> menuIds;
|
||||
}
|
||||
21
src/main/java/com/test/demo/bo/RoleQueryBo.java
Normal file
21
src/main/java/com/test/demo/bo/RoleQueryBo.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "RoleQueryBo", description = "角色查询参数")
|
||||
public class RoleQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "角色编码/名称关键字", example = "管理员")
|
||||
private String keyword;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/TaskQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/TaskQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 定时任务查询参数
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "TaskQueryBo", description = "定时任务查询参数")
|
||||
public class TaskQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "任务名称", example = "清理")
|
||||
private String taskName;
|
||||
|
||||
@Schema(description = "Bean名称", example = "fileService")
|
||||
private String beanName;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
44
src/main/java/com/test/demo/bo/UserBo.java
Normal file
44
src/main/java/com/test/demo/bo/UserBo.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 用户新增/编辑 Bo
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserBo", description = "用户请求参数")
|
||||
public class UserBo {
|
||||
|
||||
@NotNull(groups = ValidGroup.Update.class, message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID(编辑时必填)", example = "1")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "用户名不能为空")
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@NotBlank(groups = ValidGroup.Add.class, message = "密码不能为空")
|
||||
@Schema(description = "密码", example = "admin123")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "昵称", example = "管理员")
|
||||
private String nickname;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Schema(description = "邮箱", example = "admin@example.com")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "头像路径", example = "/avatar/admin.png")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
24
src/main/java/com/test/demo/bo/UserQueryBo.java
Normal file
24
src/main/java/com/test/demo/bo/UserQueryBo.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import com.test.demo.common.base.BaseBo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户查询 Bo
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "UserQueryBo", description = "用户查询参数")
|
||||
public class UserQueryBo extends BaseBo {
|
||||
|
||||
@Schema(description = "用户名(模糊查询)", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
}
|
||||
22
src/main/java/com/test/demo/bo/UserRoleAssignBo.java
Normal file
22
src/main/java/com/test/demo/bo/UserRoleAssignBo.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.bo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色分配参数
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "UserRoleAssignBo", description = "用户角色分配参数")
|
||||
public class UserRoleAssignBo {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色ID列表")
|
||||
private List<Long> roleIds;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.test.demo.serializer.DesensitizeSerializer;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 数据脱敏注解(标注在 VO 字段上)
|
||||
*/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@JacksonAnnotationsInside
|
||||
@JsonSerialize(using = DesensitizeSerializer.class)
|
||||
public @interface Desensitize {
|
||||
|
||||
/**
|
||||
* 脱敏类型
|
||||
*/
|
||||
DesensitizeType type();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
/**
|
||||
* 脱敏类型枚举
|
||||
*/
|
||||
public enum DesensitizeType {
|
||||
/** 手机号 */
|
||||
PHONE,
|
||||
/** 身份证 */
|
||||
ID_CARD,
|
||||
/** 邮箱 */
|
||||
EMAIL,
|
||||
/** 银行卡 */
|
||||
BANK_CARD,
|
||||
/** 姓名 */
|
||||
NAME,
|
||||
/** 地址 */
|
||||
ADDRESS
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 接口幂等性注解(用于插入类接口)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Idempotent {
|
||||
|
||||
/** 幂等有效期(秒) */
|
||||
int expireTime() default 5;
|
||||
|
||||
/** 提示信息 */
|
||||
String message() default "请勿重复提交";
|
||||
}
|
||||
30
src/main/java/com/test/demo/common/annotation/Log.java
Normal file
30
src/main/java/com/test/demo/common/annotation/Log.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import com.test.demo.enums.LogType;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Log {
|
||||
|
||||
/** 操作说明(与 Knife4j @Operation summary 保持一致) */
|
||||
String value() default "";
|
||||
|
||||
/** 日志类型 */
|
||||
LogType type() default LogType.OPER;
|
||||
|
||||
/** 是否写入数据库 */
|
||||
boolean saveToDB() default false;
|
||||
|
||||
/** 日志等级 */
|
||||
LogLevel level() default LogLevel.INFO;
|
||||
|
||||
enum LogLevel {
|
||||
DEBUG, INFO, WARN, ERROR
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/test/demo/common/annotation/RateLimit.java
Normal file
21
src/main/java/com/test/demo/common/annotation/RateLimit.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.test.demo.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 限流注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
/** 限流次数 */
|
||||
int count() default 10;
|
||||
|
||||
/** 时间窗口(秒) */
|
||||
int time() default 60;
|
||||
|
||||
/** 限流 key(默认 IP+方法名) */
|
||||
String key() default "";
|
||||
}
|
||||
29
src/main/java/com/test/demo/common/base/BaseBo.java
Normal file
29
src/main/java/com/test/demo/common/base/BaseBo.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Bo 基类(分页参数)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页请求基类")
|
||||
public class BaseBo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Min(value = 1, message = "页码最小为1")
|
||||
@Schema(description = "页码", example = "1", defaultValue = "1")
|
||||
private Integer pageNum = 1;
|
||||
|
||||
@Min(value = 1, message = "每页条数最小为1")
|
||||
@Max(value = 500, message = "每页条数最大为500")
|
||||
@Schema(description = "每页条数", example = "10", defaultValue = "10")
|
||||
private Integer pageSize = 10;
|
||||
}
|
||||
36
src/main/java/com/test/demo/common/base/BaseEntity.java
Normal file
36
src/main/java/com/test/demo/common/base/BaseEntity.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entity 基类
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "实体基类")
|
||||
public abstract class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@TableLogic
|
||||
@Schema(description = "逻辑删除标记", example = "0")
|
||||
private Integer deleted;
|
||||
}
|
||||
49
src/main/java/com/test/demo/common/base/PageResult.java
Normal file
49
src/main/java/com/test/demo/common/base/PageResult.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.common.base;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页返回封装
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分页响应结果")
|
||||
public class PageResult<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "当前页数据列表")
|
||||
private List<T> list;
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
@Schema(description = "当前页码", example = "1")
|
||||
private Integer pageNum;
|
||||
@Schema(description = "每页条数", example = "10")
|
||||
private Integer pageSize;
|
||||
@Schema(description = "总页数", example = "10")
|
||||
private Integer totalPages;
|
||||
|
||||
public PageResult() {}
|
||||
|
||||
public PageResult(List<T> list, Long total, Integer pageNum, Integer pageSize) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
this.pageNum = pageNum;
|
||||
this.pageSize = pageSize;
|
||||
this.totalPages = (int) Math.ceil((double) total / pageSize);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> of(List<T> list, Long total, Integer pageNum, Integer pageSize) {
|
||||
return new PageResult<>(list, total, pageNum, pageSize);
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty(Integer pageNum, Integer pageSize) {
|
||||
return new PageResult<>(Collections.emptyList(), 0L, pageNum, pageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 通用常量
|
||||
*/
|
||||
public final class CommonConstant {
|
||||
|
||||
private CommonConstant() {}
|
||||
|
||||
/** 成功标记 */
|
||||
public static final int SUCCESS = 200;
|
||||
|
||||
/** 失败标记 */
|
||||
public static final int FAIL = 500;
|
||||
|
||||
/** UTF-8 编码 */
|
||||
public static final String UTF8 = "UTF-8";
|
||||
|
||||
/** 默认日期格式 */
|
||||
public static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
|
||||
/** 默认日期时间格式 */
|
||||
public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/** 未知用户 */
|
||||
public static final String UNKNOWN_USER = "unknown";
|
||||
|
||||
/** 签名请求头 */
|
||||
public static final String HEADER_TIMESTAMP = "X-Timestamp";
|
||||
public static final String HEADER_NONCE = "X-Nonce";
|
||||
public static final String HEADER_SIGN = "X-Sign";
|
||||
|
||||
/** 幂等性请求头 */
|
||||
public static final String HEADER_IDEMPOTENT_TOKEN = "X-Idempotent-Token";
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 枚举/字典来源类型
|
||||
*/
|
||||
public final class EnumSourceType {
|
||||
|
||||
private EnumSourceType() {}
|
||||
|
||||
public static final String SYSTEM = "SYSTEM";
|
||||
|
||||
public static final String CUSTOM = "CUSTOM";
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* 权限标识常量
|
||||
*/
|
||||
public final class PermissionConstant {
|
||||
|
||||
private PermissionConstant() {}
|
||||
|
||||
public static final String USER_PAGE = "system:user:page";
|
||||
public static final String USER_DETAIL = "system:user:detail";
|
||||
public static final String USER_ADD = "system:user:add";
|
||||
public static final String USER_EDIT = "system:user:edit";
|
||||
public static final String USER_REMOVE = "system:user:remove";
|
||||
public static final String USER_RESET_PASSWORD = "system:user:reset-password";
|
||||
public static final String USER_IMPORT = "system:user:import";
|
||||
public static final String USER_EXPORT = "system:user:export";
|
||||
|
||||
public static final String ROLE_PAGE = "system:role:page";
|
||||
public static final String ROLE_LIST = "system:role:list";
|
||||
public static final String ROLE_ADD = "system:role:add";
|
||||
public static final String ROLE_EDIT = "system:role:edit";
|
||||
public static final String ROLE_REMOVE = "system:role:remove";
|
||||
public static final String ROLE_ASSIGN_MENU = "system:role:assign-menu";
|
||||
public static final String ROLE_ASSIGN_USER = "system:role:assign-user";
|
||||
|
||||
public static final String MENU_LIST = "system:menu:list";
|
||||
public static final String MENU_ADD = "system:menu:add";
|
||||
public static final String MENU_EDIT = "system:menu:edit";
|
||||
public static final String MENU_REMOVE = "system:menu:remove";
|
||||
public static final String MENU_CURRENT = "system:menu:current";
|
||||
|
||||
public static final String ENUM_TYPE_PAGE = "system:enum:type:page";
|
||||
public static final String ENUM_TYPE_ADD = "system:enum:type:add";
|
||||
public static final String ENUM_TYPE_EDIT = "system:enum:type:edit";
|
||||
public static final String ENUM_TYPE_ENABLE = "system:enum:type:enable";
|
||||
public static final String ENUM_TYPE_REMOVE = "system:enum:type:remove";
|
||||
public static final String ENUM_ITEM_LIST = "system:enum:item:list";
|
||||
public static final String ENUM_ITEM_ADD = "system:enum:item:add";
|
||||
public static final String ENUM_ITEM_EDIT = "system:enum:item:edit";
|
||||
public static final String ENUM_ITEM_ENABLE = "system:enum:item:enable";
|
||||
public static final String ENUM_ITEM_REMOVE = "system:enum:item:remove";
|
||||
|
||||
public static final String TASK_PAGE = "system:task:page";
|
||||
public static final String TASK_DETAIL = "system:task:detail";
|
||||
public static final String TASK_ADD = "system:task:add";
|
||||
public static final String TASK_EDIT = "system:task:edit";
|
||||
public static final String TASK_PAUSE = "system:task:pause";
|
||||
public static final String TASK_RESUME = "system:task:resume";
|
||||
public static final String TASK_REMOVE = "system:task:remove";
|
||||
|
||||
public static final String FILE_LIST = "system:file:list";
|
||||
public static final String FILE_UPLOAD = "system:file:upload";
|
||||
public static final String FILE_DOWNLOAD = "system:file:download";
|
||||
public static final String FILE_DELETE = "system:file:delete";
|
||||
|
||||
public static final String LOGIN_LOG_PAGE = "system:login-log:page";
|
||||
public static final String LOGIN_LOG_EXPORT = "system:login-log:export";
|
||||
public static final String OPER_LOG_PAGE = "system:oper-log:page";
|
||||
public static final String OPER_LOG_EXPORT = "system:oper-log:export";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.test.demo.common.constant;
|
||||
|
||||
/**
|
||||
* Redis Key 常量
|
||||
*/
|
||||
public final class RedisKeyConstant {
|
||||
|
||||
private RedisKeyConstant() {}
|
||||
|
||||
/** 前缀 */
|
||||
private static final String PREFIX = "demo:";
|
||||
|
||||
/** 验证码 */
|
||||
public static final String CAPTCHA = PREFIX + "captcha:";
|
||||
|
||||
/** 限流 */
|
||||
public static final String RATE_LIMIT = PREFIX + "rate_limit:";
|
||||
|
||||
/** 幂等性 */
|
||||
public static final String IDEMPOTENT = PREFIX + "idempotent:";
|
||||
|
||||
/** 防重放 nonce */
|
||||
public static final String NONCE = PREFIX + "nonce:";
|
||||
|
||||
/** 分布式锁 */
|
||||
public static final String LOCK = PREFIX + "lock:";
|
||||
|
||||
/** 用户权限缓存 */
|
||||
public static final String USER_PERMISSION = PREFIX + "user:permission:";
|
||||
|
||||
/** 用户角色缓存 */
|
||||
public static final String USER_ROLE = PREFIX + "user:role:";
|
||||
|
||||
/** 登录失败次数 */
|
||||
public static final String LOGIN_FAIL = PREFIX + "login:fail:";
|
||||
}
|
||||
15
src/main/java/com/test/demo/common/enums/EnumDesc.java
Normal file
15
src/main/java/com/test/demo/common/enums/EnumDesc.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 枚举描述注解 - 标记在枚举类上,提供枚举说明信息
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface EnumDesc {
|
||||
|
||||
/** 枚举说明 */
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 枚举 Jackson 反序列化器 - 按 value 匹配枚举
|
||||
*/
|
||||
public class EnumDeserializer extends JsonDeserializer<IBaseEnum<?>> implements ContextualDeserializer {
|
||||
|
||||
private Class<?> enumClass;
|
||||
|
||||
public EnumDeserializer() {}
|
||||
|
||||
public EnumDeserializer(Class<?> enumClass) {
|
||||
this.enumClass = enumClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public IBaseEnum<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (enumClass == null) {
|
||||
return null;
|
||||
}
|
||||
String text = p.getText();
|
||||
for (Object constant : enumClass.getEnumConstants()) {
|
||||
IBaseEnum baseEnum = (IBaseEnum) constant;
|
||||
if (String.valueOf(baseEnum.getValue()).equals(text)) {
|
||||
return baseEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
|
||||
JavaType type = ctxt.getContextualType();
|
||||
if (type == null && property != null) {
|
||||
type = property.getType();
|
||||
}
|
||||
if (type != null) {
|
||||
Class<?> rawClass = type.getRawClass();
|
||||
if (IBaseEnum.class.isAssignableFrom(rawClass)) {
|
||||
return new EnumDeserializer(rawClass);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
134
src/main/java/com/test/demo/common/enums/EnumRegistry.java
Normal file
134
src/main/java/com/test/demo/common/enums/EnumRegistry.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.test.demo.common.constant.EnumSourceType;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统枚举注册中心 - 启动时扫描并同步到数据库
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class EnumRegistry implements ApplicationRunner {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
syncEnums("com.test.demo");
|
||||
}
|
||||
|
||||
private void syncEnums(String basePackage) throws Exception {
|
||||
String pattern = "classpath*:" + basePackage.replace('.', '/') + "/**/enums/**/*.class";
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(resolver);
|
||||
Resource[] resources = resolver.getResources(pattern);
|
||||
int count = 0;
|
||||
|
||||
for (Resource resource : resources) {
|
||||
MetadataReader reader = factory.getMetadataReader(resource);
|
||||
String className = reader.getClassMetadata().getClassName();
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
if (clazz.isEnum() && IBaseEnum.class.isAssignableFrom(clazz)) {
|
||||
EnumDesc desc = clazz.getAnnotation(EnumDesc.class);
|
||||
if (desc != null) {
|
||||
syncEnum(clazz, desc);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.warn("枚举类加载失败: {}", className);
|
||||
}
|
||||
}
|
||||
log.info("系统枚举同步完成, 共处理 {} 个带注解枚举", count);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
protected void syncEnum(Class<?> clazz, EnumDesc desc) {
|
||||
String enumCode = clazz.getSimpleName();
|
||||
EnumTypeEntity existingType = enumTypeService.getByEnumCode(enumCode);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (existingType != null && EnumSourceType.CUSTOM.equals(existingType.getSourceType())) {
|
||||
log.warn("检测到自定义字典与系统枚举编码冲突, 已跳过同步: {}", enumCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingType == null) {
|
||||
existingType = new EnumTypeEntity();
|
||||
existingType.setEnumCode(enumCode);
|
||||
existingType.setSourceType(EnumSourceType.SYSTEM);
|
||||
existingType.setBuiltin(1);
|
||||
existingType.setEnabled(1);
|
||||
existingType.setSyncEnabled(1);
|
||||
existingType.setSort(0);
|
||||
} else if (existingType.getSyncEnabled() != null && existingType.getSyncEnabled() == 0) {
|
||||
log.info("系统枚举已关闭同步更新, 跳过: {}", enumCode);
|
||||
return;
|
||||
}
|
||||
|
||||
existingType.setEnumName(existingType.getEnumName() == null ? enumCode : existingType.getEnumName());
|
||||
existingType.setEnumDesc(desc.value());
|
||||
existingType.setClassName(clazz.getName());
|
||||
existingType.setPackageName(clazz.getPackageName());
|
||||
Object[] constants = clazz.getEnumConstants();
|
||||
Object firstValue = constants.length == 0 ? null : ((IBaseEnum) constants[0]).getValue();
|
||||
existingType.setValueType(firstValue == null ? "java.lang.Object" : firstValue.getClass().getName());
|
||||
existingType.setLastSyncTime(now);
|
||||
|
||||
if (existingType.getId() == null) {
|
||||
enumTypeService.save(existingType);
|
||||
} else {
|
||||
enumTypeService.updateById(existingType);
|
||||
}
|
||||
|
||||
List<EnumItemEntity> systemItems = new ArrayList<>();
|
||||
int sort = 0;
|
||||
for (Object constant : clazz.getEnumConstants()) {
|
||||
IBaseEnum baseEnum = (IBaseEnum) constant;
|
||||
EnumItemEntity item = new EnumItemEntity();
|
||||
item.setEnumCode(enumCode);
|
||||
item.setItemValue(String.valueOf(baseEnum.getValue()));
|
||||
item.setItemLabel(baseEnum.getDesc());
|
||||
item.setItemName(((Enum<?>) constant).name());
|
||||
item.setSourceType(EnumSourceType.SYSTEM);
|
||||
item.setBuiltin(1);
|
||||
item.setEnabled(1);
|
||||
item.setEditable(1);
|
||||
item.setDeletable(0);
|
||||
item.setSort(sort++);
|
||||
item.setLastSyncTime(now);
|
||||
systemItems.add(item);
|
||||
}
|
||||
enumItemService.syncSystemItems(enumCode, systemItems, now);
|
||||
|
||||
long staleCount = enumItemService.count(new LambdaQueryWrapper<EnumItemEntity>()
|
||||
.eq(EnumItemEntity::getEnumCode, enumCode)
|
||||
.eq(EnumItemEntity::getSourceType, EnumSourceType.SYSTEM)
|
||||
.eq(EnumItemEntity::getEnabled, 0));
|
||||
if (staleCount > 0) {
|
||||
log.info("系统枚举 {} 存在 {} 个失效项,已自动置为停用", enumCode, staleCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/test/demo/common/enums/EnumSerializer.java
Normal file
22
src/main/java/com/test/demo/common/enums/EnumSerializer.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 枚举 Jackson 序列化器 - 输出 {value, desc}
|
||||
*/
|
||||
public class EnumSerializer extends JsonSerializer<IBaseEnum<?>> {
|
||||
|
||||
@Override
|
||||
public void serialize(IBaseEnum<?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeStartObject();
|
||||
gen.writeFieldName("value");
|
||||
gen.writeObject(value.getValue());
|
||||
gen.writeStringField("desc", value.getDesc());
|
||||
gen.writeEndObject();
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/test/demo/common/enums/IBaseEnum.java
Normal file
13
src/main/java/com/test/demo/common/enums/IBaseEnum.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.common.enums;
|
||||
|
||||
/**
|
||||
* 枚举基础接口 - 所有业务枚举需实现此接口
|
||||
*/
|
||||
public interface IBaseEnum<T> {
|
||||
|
||||
/** 获取枚举值(存入数据库) */
|
||||
T getValue();
|
||||
|
||||
/** 获取枚举描述 */
|
||||
String getDesc();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.test.demo.common.exception;
|
||||
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 自定义业务异常
|
||||
*/
|
||||
@Getter
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int code;
|
||||
|
||||
public BizException(String msg) {
|
||||
super(msg);
|
||||
this.code = ResultCode.INTERNAL_ERROR.getCode();
|
||||
}
|
||||
|
||||
public BizException(int code, String msg) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BizException(ResultCode resultCode) {
|
||||
super(resultCode.getMsg());
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
|
||||
public BizException(ResultCode resultCode, String msg) {
|
||||
super(msg);
|
||||
this.code = resultCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷抛出异常
|
||||
*/
|
||||
public static void throwEx(String msg) {
|
||||
throw new BizException(msg);
|
||||
}
|
||||
|
||||
public static void throwEx(ResultCode resultCode) {
|
||||
throw new BizException(resultCode);
|
||||
}
|
||||
|
||||
public static void throwEx(int code, String msg) {
|
||||
throw new BizException(code, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.test.demo.common.exception;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotPermissionException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizException.class)
|
||||
public ResponseEntity<R<Void>> handleBizException(BizException e) {
|
||||
log.warn("业务异常: {}", e.getMessage());
|
||||
return ResponseEntity.status(resolveStatus(e.getCode()))
|
||||
.body(R.fail(e.getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<R<Void>> handleValidException(MethodArgumentNotValidException e) {
|
||||
String msg = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("参数校验失败: {}", msg);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, msg));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public ResponseEntity<R<Void>> handleConstraintViolation(ConstraintViolationException e) {
|
||||
String msg = e.getConstraintViolations().stream()
|
||||
.map(ConstraintViolation::getMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("约束校验失败: {}", msg);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, msg));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public ResponseEntity<R<Void>> handleMissingParam(MissingServletRequestParameterException e) {
|
||||
log.warn("缺少请求参数: {}", e.getParameterName());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, "缺少参数: " + e.getParameterName()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
public ResponseEntity<R<Void>> handleNotLogin(NotLoginException e) {
|
||||
log.warn("未登录访问: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(R.fail(ResultCode.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotPermissionException.class)
|
||||
public ResponseEntity<R<Void>> handleNotPermission(NotPermissionException e) {
|
||||
log.warn("无权限: {}", e.getPermission());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(R.fail(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotRoleException.class)
|
||||
public ResponseEntity<R<Void>> handleNotRole(NotRoleException e) {
|
||||
log.warn("无角色: {}", e.getRole());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(R.fail(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
public ResponseEntity<R<Void>> handleMaxUploadSize(MaxUploadSizeExceededException e) {
|
||||
log.warn("上传文件过大: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(R.fail(ResultCode.BAD_REQUEST, "上传文件大小超出限制"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public ResponseEntity<R<Void>> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
|
||||
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
.body(R.fail(ResultCode.METHOD_NOT_ALLOWED));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public ResponseEntity<R<Void>> handleNoResource(NoResourceFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(R.fail(ResultCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<R<Void>> handleException(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(R.fail(ResultCode.INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
private HttpStatus resolveStatus(int code) {
|
||||
return switch (code) {
|
||||
case 400 -> HttpStatus.BAD_REQUEST;
|
||||
case 401 -> HttpStatus.UNAUTHORIZED;
|
||||
case 403 -> HttpStatus.FORBIDDEN;
|
||||
case 404 -> HttpStatus.NOT_FOUND;
|
||||
case 405 -> HttpStatus.METHOD_NOT_ALLOWED;
|
||||
case 409 -> HttpStatus.CONFLICT;
|
||||
case 415 -> HttpStatus.UNSUPPORTED_MEDIA_TYPE;
|
||||
case 423 -> HttpStatus.LOCKED;
|
||||
case 429 -> HttpStatus.TOO_MANY_REQUESTS;
|
||||
default -> HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
};
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/test/demo/common/result/R.java
Normal file
63
src/main/java/com/test/demo/common/result/R.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.test.demo.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统一返回结果
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class R<T> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int code;
|
||||
private boolean success;
|
||||
private String msg;
|
||||
private T data;
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
private R() {}
|
||||
|
||||
private R(int code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.success = code == ResultCode.SUCCESS.getCode();
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public static <T> R<T> ok() {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(T data) {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> R<T> ok(T data, String msg) {
|
||||
return new R<>(ResultCode.SUCCESS.getCode(), msg, data);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(String msg) {
|
||||
return new R<>(ResultCode.INTERNAL_ERROR.getCode(), msg, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(ResultCode resultCode) {
|
||||
return new R<>(resultCode.getCode(), resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(ResultCode resultCode, String msg) {
|
||||
return new R<>(resultCode.getCode(), msg, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> fail(int code, String msg) {
|
||||
return new R<>(code, msg, null);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/test/demo/common/result/ResultCode.java
Normal file
30
src/main/java/com/test/demo/common/result/ResultCode.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.common.result;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 返回码枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ResultCode {
|
||||
|
||||
SUCCESS(200, "操作成功"),
|
||||
BAD_REQUEST(400, "请求参数错误"),
|
||||
UNAUTHORIZED(401, "未登录或登录已过期"),
|
||||
FORBIDDEN(403, "无权限访问"),
|
||||
NOT_FOUND(404, "资源不存在"),
|
||||
CONFLICT(409, "资源冲突"),
|
||||
METHOD_NOT_ALLOWED(405, "请求方法不允许"),
|
||||
UNSUPPORTED_MEDIA_TYPE(415, "不支持的资源类型"),
|
||||
ACCOUNT_LOCKED(423, "账号已锁定"),
|
||||
RATE_LIMIT(429, "请求过于频繁"),
|
||||
INTERNAL_ERROR(500, "系统内部错误"),
|
||||
IDEMPOTENT_REJECT(601, "请勿重复提交"),
|
||||
SIGNATURE_INVALID(602, "签名校验失败"),
|
||||
REPLAY_ATTACK(603, "请求已过期或重复");
|
||||
|
||||
private final int code;
|
||||
private final String msg;
|
||||
}
|
||||
19
src/main/java/com/test/demo/common/validate/ValidGroup.java
Normal file
19
src/main/java/com/test/demo/common/validate/ValidGroup.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.test.demo.common.validate;
|
||||
|
||||
/**
|
||||
* 参数校验分组
|
||||
*/
|
||||
public interface ValidGroup {
|
||||
|
||||
/** 新增 */
|
||||
interface Add {}
|
||||
|
||||
/** 修改 */
|
||||
interface Update {}
|
||||
|
||||
/** 查询 */
|
||||
interface Query {}
|
||||
|
||||
/** 删除 */
|
||||
interface Delete {}
|
||||
}
|
||||
204
src/main/java/com/test/demo/config/AuthDataInitializer.java
Normal file
204
src/main/java/com/test/demo/config/AuthDataInitializer.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import com.test.demo.entity.RoleMenuEntity;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.entity.UserRoleEntity;
|
||||
import com.test.demo.mapper.MenuMapper;
|
||||
import com.test.demo.mapper.RoleMapper;
|
||||
import com.test.demo.mapper.RoleMenuMapper;
|
||||
import com.test.demo.mapper.UserRoleMapper;
|
||||
import com.test.demo.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限基础数据初始化
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AuthDataInitializer implements ApplicationRunner {
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final MenuMapper menuMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void run(ApplicationArguments args) {
|
||||
RoleEntity adminRole = ensureAdminRole();
|
||||
List<Long> menuIds = ensureMenus();
|
||||
bindRoleMenus(adminRole.getId(), menuIds);
|
||||
bindAdminUser(adminRole.getId());
|
||||
}
|
||||
|
||||
private RoleEntity ensureAdminRole() {
|
||||
RoleEntity role = roleMapper.selectOne(new LambdaQueryWrapper<RoleEntity>()
|
||||
.eq(RoleEntity::getRoleCode, "ADMIN")
|
||||
.last("limit 1"));
|
||||
if (role != null) {
|
||||
return role;
|
||||
}
|
||||
role = new RoleEntity();
|
||||
role.setRoleCode("ADMIN");
|
||||
role.setRoleName("系统管理员");
|
||||
role.setEnabled(1);
|
||||
role.setSort(0);
|
||||
role.setRemark("系统初始化管理员角色");
|
||||
roleMapper.insert(role);
|
||||
return role;
|
||||
}
|
||||
|
||||
private List<Long> ensureMenus() {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
Long userRoot = ensureMenu(0L, "MENU", "用户管理", "/user", "system/user/index", null, "User", 1, 1, 10, "用户管理菜单");
|
||||
ids.add(userRoot);
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "用户分页", null, null, PermissionConstant.USER_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "用户详情", null, null, PermissionConstant.USER_DETAIL, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "新增用户", null, null, PermissionConstant.USER_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "编辑用户", null, null, PermissionConstant.USER_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "删除用户", null, null, PermissionConstant.USER_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "重置密码", null, null, PermissionConstant.USER_RESET_PASSWORD, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "导入用户", null, null, PermissionConstant.USER_IMPORT, null, 1, 1, 7, null));
|
||||
ids.add(ensureMenu(userRoot, "BUTTON", "导出用户", null, null, PermissionConstant.USER_EXPORT, null, 1, 1, 8, null));
|
||||
|
||||
Long roleRoot = ensureMenu(0L, "MENU", "角色管理", "/role", "system/role/index", null, "Lock", 1, 1, 20, "角色管理菜单");
|
||||
ids.add(roleRoot);
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "角色分页", null, null, PermissionConstant.ROLE_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "角色列表", null, null, PermissionConstant.ROLE_LIST, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "新增角色", null, null, PermissionConstant.ROLE_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "编辑角色", null, null, PermissionConstant.ROLE_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "删除角色", null, null, PermissionConstant.ROLE_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "分配菜单", null, null, PermissionConstant.ROLE_ASSIGN_MENU, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(roleRoot, "BUTTON", "分配用户", null, null, PermissionConstant.ROLE_ASSIGN_USER, null, 1, 1, 7, null));
|
||||
|
||||
Long menuRoot = ensureMenu(0L, "MENU", "菜单管理", "/menu", "system/menu/index", null, "Menu", 1, 1, 30, "菜单权限管理");
|
||||
ids.add(menuRoot);
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "菜单列表", null, null, PermissionConstant.MENU_LIST, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "新增菜单", null, null, PermissionConstant.MENU_ADD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "编辑菜单", null, null, PermissionConstant.MENU_EDIT, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "删除菜单", null, null, PermissionConstant.MENU_REMOVE, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(menuRoot, "BUTTON", "当前菜单", null, null, PermissionConstant.MENU_CURRENT, null, 1, 1, 5, null));
|
||||
|
||||
Long enumRoot = ensureMenu(0L, "MENU", "字典管理", "/enum-manage", "system/enum/index", null, "List", 1, 1, 40, "字典管理");
|
||||
ids.add(enumRoot);
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "类型分页", null, null, PermissionConstant.ENUM_TYPE_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "新增类型", null, null, PermissionConstant.ENUM_TYPE_ADD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "编辑类型", null, null, PermissionConstant.ENUM_TYPE_EDIT, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "启停类型", null, null, PermissionConstant.ENUM_TYPE_ENABLE, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "删除类型", null, null, PermissionConstant.ENUM_TYPE_REMOVE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "枚举项列表", null, null, PermissionConstant.ENUM_ITEM_LIST, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "新增枚举项", null, null, PermissionConstant.ENUM_ITEM_ADD, null, 1, 1, 7, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "编辑枚举项", null, null, PermissionConstant.ENUM_ITEM_EDIT, null, 1, 1, 8, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "启停枚举项", null, null, PermissionConstant.ENUM_ITEM_ENABLE, null, 1, 1, 9, null));
|
||||
ids.add(ensureMenu(enumRoot, "BUTTON", "删除枚举项", null, null, PermissionConstant.ENUM_ITEM_REMOVE, null, 1, 1, 10, null));
|
||||
|
||||
Long taskRoot = ensureMenu(0L, "MENU", "定时任务", "/task", "system/task/index", null, "Clock", 1, 1, 50, "定时任务管理");
|
||||
ids.add(taskRoot);
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "任务分页", null, null, PermissionConstant.TASK_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "任务详情", null, null, PermissionConstant.TASK_DETAIL, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "新增任务", null, null, PermissionConstant.TASK_ADD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "编辑任务", null, null, PermissionConstant.TASK_EDIT, null, 1, 1, 4, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "暂停任务", null, null, PermissionConstant.TASK_PAUSE, null, 1, 1, 5, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "恢复任务", null, null, PermissionConstant.TASK_RESUME, null, 1, 1, 6, null));
|
||||
ids.add(ensureMenu(taskRoot, "BUTTON", "删除任务", null, null, PermissionConstant.TASK_REMOVE, null, 1, 1, 7, null));
|
||||
|
||||
Long fileRoot = ensureMenu(0L, "MENU", "文件管理", "/file", "system/file/index", null, "Folder", 1, 1, 60, "文件管理");
|
||||
ids.add(fileRoot);
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "文件列表", null, null, PermissionConstant.FILE_LIST, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "上传文件", null, null, PermissionConstant.FILE_UPLOAD, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "下载文件", null, null, PermissionConstant.FILE_DOWNLOAD, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(fileRoot, "BUTTON", "删除文件", null, null, PermissionConstant.FILE_DELETE, null, 1, 1, 4, null));
|
||||
|
||||
Long logRoot = ensureMenu(0L, "MENU", "日志管理", "/log", "system/log/index", null, "Document", 1, 1, 70, "日志管理");
|
||||
ids.add(logRoot);
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "登录日志分页", null, null, PermissionConstant.LOGIN_LOG_PAGE, null, 1, 1, 1, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "登录日志导出", null, null, PermissionConstant.LOGIN_LOG_EXPORT, null, 1, 1, 2, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "操作日志分页", null, null, PermissionConstant.OPER_LOG_PAGE, null, 1, 1, 3, null));
|
||||
ids.add(ensureMenu(logRoot, "BUTTON", "操作日志导出", null, null, PermissionConstant.OPER_LOG_EXPORT, null, 1, 1, 4, null));
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Long ensureMenu(Long parentId, String menuType, String menuName, String path, String component,
|
||||
String permission, String icon, Integer visible, Integer enabled, Integer sort, String remark) {
|
||||
MenuEntity existing = menuMapper.selectOne(new LambdaQueryWrapper<MenuEntity>()
|
||||
.eq(MenuEntity::getParentId, parentId)
|
||||
.eq(MenuEntity::getMenuName, menuName)
|
||||
.last("limit 1"));
|
||||
if (existing != null) {
|
||||
existing.setMenuType(menuType);
|
||||
existing.setPath(path);
|
||||
existing.setComponent(component);
|
||||
existing.setPermission(permission);
|
||||
existing.setIcon(icon);
|
||||
existing.setVisible(visible);
|
||||
existing.setEnabled(enabled);
|
||||
existing.setSort(sort);
|
||||
existing.setRemark(remark);
|
||||
menuMapper.updateById(existing);
|
||||
return existing.getId();
|
||||
}
|
||||
MenuEntity entity = new MenuEntity();
|
||||
entity.setParentId(parentId);
|
||||
entity.setMenuType(menuType);
|
||||
entity.setMenuName(menuName);
|
||||
entity.setPath(path);
|
||||
entity.setComponent(component);
|
||||
entity.setPermission(permission);
|
||||
entity.setIcon(icon);
|
||||
entity.setVisible(visible);
|
||||
entity.setEnabled(enabled);
|
||||
entity.setSort(sort);
|
||||
entity.setRemark(remark);
|
||||
menuMapper.insert(entity);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private void bindRoleMenus(Long roleId, List<Long> menuIds) {
|
||||
for (Long menuId : menuIds) {
|
||||
Long count = roleMenuMapper.selectCount(new LambdaQueryWrapper<RoleMenuEntity>()
|
||||
.eq(RoleMenuEntity::getRoleId, roleId)
|
||||
.eq(RoleMenuEntity::getMenuId, menuId));
|
||||
if (count == 0) {
|
||||
RoleMenuEntity entity = new RoleMenuEntity();
|
||||
entity.setRoleId(roleId);
|
||||
entity.setMenuId(menuId);
|
||||
roleMenuMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bindAdminUser(Long roleId) {
|
||||
UserEntity admin = userMapper.selectOne(new LambdaQueryWrapper<UserEntity>()
|
||||
.eq(UserEntity::getUsername, "admin")
|
||||
.last("limit 1"));
|
||||
if (admin == null) {
|
||||
log.warn("未找到 admin 用户,已跳过管理员角色绑定");
|
||||
return;
|
||||
}
|
||||
Long count = userRoleMapper.selectCount(new LambdaQueryWrapper<UserRoleEntity>()
|
||||
.eq(UserRoleEntity::getUserId, admin.getId())
|
||||
.eq(UserRoleEntity::getRoleId, roleId));
|
||||
if (count == 0) {
|
||||
UserRoleEntity entity = new UserRoleEntity();
|
||||
entity.setUserId(admin.getId());
|
||||
entity.setRoleId(roleId);
|
||||
userRoleMapper.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/java/com/test/demo/config/CaffeineCacheConfig.java
Normal file
29
src/main/java/com/test/demo/config/CaffeineCacheConfig.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Caffeine 本地缓存配置
|
||||
*/
|
||||
@EnableCaching
|
||||
@Configuration
|
||||
public class CaffeineCacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager caffeineCacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||
.initialCapacity(128)
|
||||
.maximumSize(1024)
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES)
|
||||
.recordStats());
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/test/demo/config/FileConfig.java
Normal file
27
src/main/java/com/test/demo/config/FileConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件上传配置
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "app.file")
|
||||
public class FileConfig {
|
||||
|
||||
/** 上传根路径 */
|
||||
private String uploadPath = "/data/upload";
|
||||
|
||||
/** 允许的文件扩展名 */
|
||||
private List<String> allowedExtensions = List.of(
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp",
|
||||
"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
|
||||
"txt", "csv", "zip", "rar", "7z",
|
||||
"mp4", "avi", "mov", "mp3", "wav"
|
||||
);
|
||||
}
|
||||
46
src/main/java/com/test/demo/config/JacksonConfig.java
Normal file
46
src/main/java/com/test/demo/config/JacksonConfig.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* Jackson 全局配置
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
|
||||
return builder -> {
|
||||
// Long → String 防前端精度丢失
|
||||
builder.serializerByType(Long.class, ToStringSerializer.instance);
|
||||
builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
|
||||
|
||||
// LocalDate 序列化/反序列化
|
||||
builder.serializerByType(LocalDate.class,
|
||||
new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||
builder.deserializerByType(LocalDate.class,
|
||||
new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
|
||||
|
||||
// LocalDateTime 序列化/反序列化
|
||||
builder.serializerByType(LocalDateTime.class,
|
||||
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)));
|
||||
builder.deserializerByType(LocalDateTime.class,
|
||||
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)));
|
||||
};
|
||||
}
|
||||
}
|
||||
24
src/main/java/com/test/demo/config/Knife4jConfig.java
Normal file
24
src/main/java/com/test/demo/config/Knife4jConfig.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j / OpenAPI 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Demo 后台管理系统 API")
|
||||
.description("Spring Boot 4 脚手架接口文档")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact().name("admin").email("admin@example.com")));
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/test/demo/config/MybatisPlusConfig.java
Normal file
27
src/main/java/com/test/demo/config/MybatisPlusConfig.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 分页插件
|
||||
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
|
||||
paginationInterceptor.setMaxLimit(500L);
|
||||
interceptor.addInnerInterceptor(paginationInterceptor);
|
||||
// 防全表更新与删除
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
31
src/main/java/com/test/demo/config/RedisConfig.java
Normal file
31
src/main/java/com/test/demo/config/RedisConfig.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 序列化配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/test/demo/config/SaTokenConfig.java
Normal file
13
src/main/java/com/test/demo/config/SaTokenConfig.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfig {
|
||||
|
||||
// 如需 JWT 模式,需引入 sa-token-jwt 依赖后启用以下配置
|
||||
// 默认使用 Redis Session 模式
|
||||
}
|
||||
25
src/main/java/com/test/demo/config/ScheduleConfig.java
Normal file
25
src/main/java/com/test/demo/config/ScheduleConfig.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
/**
|
||||
* 定时任务调度配置
|
||||
*/
|
||||
@EnableScheduling
|
||||
@Configuration
|
||||
public class ScheduleConfig {
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(5);
|
||||
scheduler.setThreadNamePrefix("schedule-");
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
scheduler.setAwaitTerminationSeconds(30);
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/test/demo/config/ThreadPoolConfig.java
Normal file
32
src/main/java/com/test/demo/config/ThreadPoolConfig.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
*/
|
||||
@EnableAsync
|
||||
@Configuration
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
@Bean("asyncExecutor")
|
||||
public Executor asyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
|
||||
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("async-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(30);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
65
src/main/java/com/test/demo/config/WebMvcConfig.java
Normal file
65
src/main/java/com/test/demo/config/WebMvcConfig.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.test.demo.interceptor.IdempotentInterceptor;
|
||||
import com.test.demo.interceptor.SignatureInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置(跨域 + 拦截器)
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final SignatureInterceptor signatureInterceptor;
|
||||
|
||||
private final IdempotentInterceptor idempotentInterceptor;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(signatureInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/doc.html",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/actuator/**"
|
||||
);
|
||||
|
||||
registry.addInterceptor(idempotentInterceptor)
|
||||
.addPathPatterns("/**");
|
||||
|
||||
// Sa-Token 路由拦截
|
||||
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/auth/login",
|
||||
"/auth/register",
|
||||
"/captcha/**",
|
||||
"/file/preview/**",
|
||||
"/enums/**",
|
||||
"/doc.html",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/swagger-resources/**",
|
||||
"/actuator/**"
|
||||
);
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/test/demo/config/XssFilterConfig.java
Normal file
30
src/main/java/com/test/demo/config/XssFilterConfig.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.config;
|
||||
|
||||
import com.test.demo.filter.XssFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* XSS 过滤器注册
|
||||
*/
|
||||
@Configuration
|
||||
public class XssFilterConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
|
||||
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(new XssFilter());
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setName("xssFilter");
|
||||
registration.setOrder(1);
|
||||
// 排除路径(如富文本接口)
|
||||
Map<String, String> initParams = new HashMap<>();
|
||||
initParams.put("excludes", "/file/upload");
|
||||
registration.setInitParameters(initParams);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/test/demo/controller/AuthController.java
Normal file
51
src/main/java/com/test/demo/controller/AuthController.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.bo.LoginBo;
|
||||
import com.test.demo.bo.RegisterBo;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.AuthService;
|
||||
import com.test.demo.vo.LoginVo;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 认证接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "认证管理")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "用户登录")
|
||||
public R<LoginVo> login(@RequestBody @Valid LoginBo bo) {
|
||||
return R.ok(authService.login(bo));
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "用户注册")
|
||||
public R<Void> register(@RequestBody @Valid RegisterBo bo) {
|
||||
authService.register(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "退出登录")
|
||||
public R<Void> logout() {
|
||||
authService.logout();
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/me")
|
||||
@Operation(summary = "获取当前登录用户")
|
||||
public R<UserVo> me() {
|
||||
return R.ok(authService.getCurrentUser());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.CaptchaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/captcha")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "验证码")
|
||||
public class CaptchaController {
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
@PostMapping("/generate")
|
||||
@Operation(summary = "获取图形验证码")
|
||||
public R<Map<String, String>> getCaptcha() {
|
||||
return R.ok(captchaService.generate());
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/test/demo/controller/EnumController.java
Normal file
43
src/main/java/com/test/demo/controller/EnumController.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 枚举查询接口(供前端下拉框使用)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/enums")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "枚举查询")
|
||||
public class EnumController {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "获取所有带描述注解的枚举元信息")
|
||||
public R<List<Map<String, Object>>> listAll() {
|
||||
return R.ok(enumTypeService.listEnabledForApi());
|
||||
}
|
||||
|
||||
@PostMapping("/items/{enumName}")
|
||||
@Operation(summary = "获取指定枚举的所有值")
|
||||
public R<List<Map<String, Object>>> getByName(
|
||||
@Parameter(description = "枚举编码", example = "LogType")
|
||||
@PathVariable String enumName) {
|
||||
return R.ok(enumItemService.listEnabledForApi(enumName));
|
||||
}
|
||||
}
|
||||
149
src/main/java/com/test/demo/controller/EnumManageController.java
Normal file
149
src/main/java/com/test/demo/controller/EnumManageController.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.EnumItemBo;
|
||||
import com.test.demo.bo.EnumTypeBo;
|
||||
import com.test.demo.bo.EnumTypeQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.EnumItemService;
|
||||
import com.test.demo.service.EnumTypeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 枚举/字典管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/enum-manage")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "枚举字典管理")
|
||||
public class EnumManageController {
|
||||
|
||||
private final EnumTypeService enumTypeService;
|
||||
private final EnumItemService enumItemService;
|
||||
|
||||
@PostMapping("/type/page")
|
||||
@Operation(summary = "分页查询枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_PAGE)
|
||||
public R<PageResult<EnumTypeEntity>> pageTypes(@RequestBody(required = false) EnumTypeQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new EnumTypeQueryBo();
|
||||
}
|
||||
return R.ok(enumTypeService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/type/add")
|
||||
@Operation(summary = "新增自定义枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_ADD)
|
||||
@Idempotent(message = "请勿重复新增枚举类型")
|
||||
@Log(value = "新增自定义枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> addType(@RequestBody @Validated(ValidGroup.Add.class) EnumTypeBo bo) {
|
||||
enumTypeService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/edit")
|
||||
@Operation(summary = "编辑枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑枚举类型")
|
||||
@Log(value = "编辑枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> editType(@RequestBody @Validated(ValidGroup.Update.class) EnumTypeBo bo) {
|
||||
enumTypeService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/enable/{id}")
|
||||
@Operation(summary = "启用或停用枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_ENABLE)
|
||||
@Idempotent(message = "请勿重复变更枚举类型状态")
|
||||
@Log(value = "变更枚举类型状态", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> changeTypeEnabled(
|
||||
@Parameter(description = "类型ID", example = "2033103240660238338")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "启用状态,1启用,0停用", example = "1")
|
||||
@RequestParam Integer enabled) {
|
||||
enumTypeService.changeEnabled(id, enabled);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/type/remove/{id}")
|
||||
@Operation(summary = "删除自定义枚举类型")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_TYPE_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除枚举类型")
|
||||
@Log(value = "删除自定义枚举类型", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> removeType(
|
||||
@Parameter(description = "类型ID", example = "2033103240660238338")
|
||||
@PathVariable Long id) {
|
||||
enumTypeService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/list/{enumCode}")
|
||||
@Operation(summary = "查询枚举项列表")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_LIST)
|
||||
public R<List<EnumItemEntity>> listItems(
|
||||
@Parameter(description = "枚举编码", example = "LogType")
|
||||
@PathVariable String enumCode) {
|
||||
return R.ok(enumItemService.listManageItems(enumCode));
|
||||
}
|
||||
|
||||
@PostMapping("/item/add")
|
||||
@Operation(summary = "新增枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_ADD)
|
||||
@Idempotent(message = "请勿重复新增枚举项")
|
||||
@Log(value = "新增枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> addItem(@RequestBody @Validated(ValidGroup.Add.class) EnumItemBo bo) {
|
||||
enumItemService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/edit")
|
||||
@Operation(summary = "编辑枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑枚举项")
|
||||
@Log(value = "编辑枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> editItem(@RequestBody @Validated(ValidGroup.Update.class) EnumItemBo bo) {
|
||||
enumItemService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/enable/{id}")
|
||||
@Operation(summary = "启用或停用枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_ENABLE)
|
||||
@Idempotent(message = "请勿重复变更枚举项状态")
|
||||
@Log(value = "变更枚举项状态", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> changeItemEnabled(
|
||||
@Parameter(description = "枚举项ID", example = "2033103240752513025")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "启用状态,1启用,0停用", example = "1")
|
||||
@RequestParam Integer enabled) {
|
||||
enumItemService.changeEnabled(id, enabled);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/item/remove/{id}")
|
||||
@Operation(summary = "删除枚举项")
|
||||
@SaCheckPermission(PermissionConstant.ENUM_ITEM_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除枚举项")
|
||||
@Log(value = "删除枚举项", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> removeItem(
|
||||
@Parameter(description = "枚举项ID", example = "2033103240752513025")
|
||||
@PathVariable Long id) {
|
||||
enumItemService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
122
src/main/java/com/test/demo/controller/FileController.java
Normal file
122
src/main/java/com/test/demo/controller/FileController.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.FileQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.FileService;
|
||||
import com.test.demo.vo.FileInfoVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 文件管理接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "文件管理")
|
||||
public class FileController {
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询文件列表")
|
||||
@SaCheckPermission(PermissionConstant.FILE_LIST)
|
||||
public R<PageResult<FileInfoVo>> list(@RequestBody(required = false) FileQueryBo query) {
|
||||
return R.ok(fileService.list(query));
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_UPLOAD)
|
||||
@Log(value = "上传文件", type = LogType.OPER)
|
||||
public R<String> upload(
|
||||
@Parameter(description = "上传文件")
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
return R.ok(fileService.upload(file));
|
||||
}
|
||||
|
||||
@GetMapping("/download/{*filePath}")
|
||||
@Operation(summary = "下载文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_DOWNLOAD)
|
||||
public void download(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String absPath = fileService.getFilePath(filePath);
|
||||
Path path = Paths.get(absPath);
|
||||
if (!Files.exists(path)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String fileName = path.getFileName().toString();
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
||||
response.setContentLengthLong(Files.size(path));
|
||||
try (InputStream is = Files.newInputStream(path);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/preview/{*filePath}")
|
||||
@Operation(summary = "预览文件")
|
||||
public void preview(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath,
|
||||
HttpServletResponse response) throws IOException {
|
||||
String absPath = fileService.getFilePath(filePath);
|
||||
Path path = Paths.get(absPath);
|
||||
if (!Files.exists(path)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String contentType = Files.probeContentType(path);
|
||||
if (contentType == null) {
|
||||
contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
response.setContentType(contentType);
|
||||
response.setContentLengthLong(Files.size(path));
|
||||
|
||||
// 视频支持 Range 请求
|
||||
if (contentType.startsWith("video/")) {
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
}
|
||||
|
||||
try (InputStream is = Files.newInputStream(path);
|
||||
OutputStream os = response.getOutputStream()) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete/{*filePath}")
|
||||
@Operation(summary = "删除文件")
|
||||
@SaCheckPermission(PermissionConstant.FILE_DELETE)
|
||||
@Log(value = "删除文件", type = LogType.OPER, saveToDB = true)
|
||||
public R<Boolean> delete(
|
||||
@Parameter(description = "文件相对路径", example = "2026/03/15/demo.xlsx")
|
||||
@PathVariable String filePath) {
|
||||
return R.ok(fileService.delete(filePath));
|
||||
}
|
||||
}
|
||||
79
src/main/java/com/test/demo/controller/LogController.java
Normal file
79
src/main/java/com/test/demo/controller/LogController.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.LoginLogQueryBo;
|
||||
import com.test.demo.bo.OperLogQueryBo;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.entity.LoginLogEntity;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import com.test.demo.service.LoginLogService;
|
||||
import com.test.demo.service.OperLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 日志管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/log")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "日志管理")
|
||||
public class LogController {
|
||||
|
||||
private final LoginLogService loginLogService;
|
||||
private final OperLogService operLogService;
|
||||
|
||||
@PostMapping("/login/page")
|
||||
@Operation(summary = "分页查询登录日志")
|
||||
@SaCheckPermission(PermissionConstant.LOGIN_LOG_PAGE)
|
||||
public R<PageResult<LoginLogEntity>> loginPage(@RequestBody(required = false) LoginLogQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new LoginLogQueryBo();
|
||||
}
|
||||
return R.ok(loginLogService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/oper/page")
|
||||
@Operation(summary = "分页查询操作日志")
|
||||
@SaCheckPermission(PermissionConstant.OPER_LOG_PAGE)
|
||||
public R<PageResult<OperLogEntity>> operPage(@RequestBody(required = false) OperLogQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new OperLogQueryBo();
|
||||
}
|
||||
return R.ok(operLogService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/login/export")
|
||||
@Operation(summary = "导出登录日志")
|
||||
@SaCheckPermission(PermissionConstant.LOGIN_LOG_EXPORT)
|
||||
public void exportLogin(
|
||||
@Parameter(description = "登录日志筛选条件")
|
||||
@RequestBody(required = false) LoginLogQueryBo query,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (query == null) {
|
||||
query = new LoginLogQueryBo();
|
||||
}
|
||||
loginLogService.export(query, response);
|
||||
}
|
||||
|
||||
@PostMapping("/oper/export")
|
||||
@Operation(summary = "导出操作日志")
|
||||
@SaCheckPermission(PermissionConstant.OPER_LOG_EXPORT)
|
||||
public void exportOper(
|
||||
@Parameter(description = "操作日志筛选条件")
|
||||
@RequestBody(required = false) OperLogQueryBo query,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if (query == null) {
|
||||
query = new OperLogQueryBo();
|
||||
}
|
||||
operLogService.export(query, response);
|
||||
}
|
||||
}
|
||||
81
src/main/java/com/test/demo/controller/MenuController.java
Normal file
81
src/main/java/com/test/demo/controller/MenuController.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.MenuBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.security.SecurityUtil;
|
||||
import com.test.demo.service.MenuService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单权限管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/menu")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "菜单权限管理")
|
||||
public class MenuController {
|
||||
|
||||
private final MenuService menuService;
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询菜单权限列表")
|
||||
@SaCheckPermission(PermissionConstant.MENU_LIST)
|
||||
public R<List<MenuEntity>> list() {
|
||||
return R.ok(menuService.listAll());
|
||||
}
|
||||
|
||||
@PostMapping("/current")
|
||||
@Operation(summary = "查询当前用户菜单")
|
||||
public R<List<MenuEntity>> current() {
|
||||
return R.ok(menuService.listCurrentUserMenus(SecurityUtil.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/permissions/current")
|
||||
@Operation(summary = "查询当前用户权限标识")
|
||||
public R<List<String>> currentPermissions() {
|
||||
return R.ok(menuService.listCurrentUserPermissions(SecurityUtil.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_ADD)
|
||||
@Idempotent(message = "请勿重复新增菜单")
|
||||
@Log(value = "新增菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) MenuBo bo) {
|
||||
menuService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑菜单")
|
||||
@Log(value = "编辑菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) MenuBo bo) {
|
||||
menuService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.MENU_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除菜单")
|
||||
@Log(value = "删除菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(@PathVariable Long id) {
|
||||
menuService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/test/demo/controller/RoleController.java
Normal file
117
src/main/java/com/test/demo/controller/RoleController.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.RoleBo;
|
||||
import com.test.demo.bo.RoleMenuAssignBo;
|
||||
import com.test.demo.bo.RoleQueryBo;
|
||||
import com.test.demo.bo.UserRoleAssignBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.RoleService;
|
||||
import com.test.demo.vo.RoleDetailVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/role")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "角色管理")
|
||||
public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_PAGE)
|
||||
public R<PageResult<RoleEntity>> page(@RequestBody(required = false) RoleQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new RoleQueryBo();
|
||||
}
|
||||
return R.ok(roleService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询启用角色列表")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_LIST)
|
||||
public R<List<RoleEntity>> list() {
|
||||
return R.ok(roleService.listEnabled());
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "查询角色详情及已分配菜单")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_LIST)
|
||||
public R<RoleDetailVo> detail(@PathVariable Long id) {
|
||||
return R.ok(roleService.getDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/user/{userId}/role-ids")
|
||||
@Operation(summary = "查询用户已分配角色ID列表")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_USER)
|
||||
public R<List<Long>> userRoleIds(@PathVariable Long userId) {
|
||||
return R.ok(roleService.listUserRoleIds(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ADD)
|
||||
@Idempotent(message = "请勿重复新增角色")
|
||||
@Log(value = "新增角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) RoleBo bo) {
|
||||
roleService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑角色")
|
||||
@Log(value = "编辑角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) RoleBo bo) {
|
||||
roleService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除角色")
|
||||
@Log(value = "删除角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(@PathVariable Long id) {
|
||||
roleService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/assign-menu")
|
||||
@Operation(summary = "分配角色菜单权限")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_MENU)
|
||||
@Idempotent(message = "请勿重复分配角色菜单")
|
||||
@Log(value = "分配角色菜单权限", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> assignMenu(@RequestBody @Validated RoleMenuAssignBo bo) {
|
||||
roleService.assignMenus(bo.getRoleId(), bo.getMenuIds());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/assign-user")
|
||||
@Operation(summary = "分配用户角色")
|
||||
@SaCheckPermission(PermissionConstant.ROLE_ASSIGN_USER)
|
||||
@Idempotent(message = "请勿重复分配用户角色")
|
||||
@Log(value = "分配用户角色", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> assignUser(@RequestBody @Validated UserRoleAssignBo bo) {
|
||||
roleService.assignUserRoles(bo.getUserId(), bo.getRoleIds());
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
95
src/main/java/com/test/demo/controller/TaskController.java
Normal file
95
src/main/java/com/test/demo/controller/TaskController.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.bo.TaskQueryBo;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.entity.ScheduledTaskEntity;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.service.DynamicTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 定时任务管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/task")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "定时任务管理")
|
||||
public class TaskController {
|
||||
|
||||
private final DynamicTaskService dynamicTaskService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_PAGE)
|
||||
public R<PageResult<ScheduledTaskEntity>> page(@RequestBody(required = false) TaskQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new TaskQueryBo();
|
||||
}
|
||||
return R.ok(dynamicTaskService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "获取定时任务详情")
|
||||
@SaCheckPermission(PermissionConstant.TASK_DETAIL)
|
||||
public R<ScheduledTaskEntity> detail(@PathVariable Long id) {
|
||||
return R.ok(dynamicTaskService.detail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "创建定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_ADD)
|
||||
@Idempotent(message = "请勿重复创建定时任务")
|
||||
@Log(value = "创建定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> create(@RequestBody ScheduledTaskEntity task) {
|
||||
dynamicTaskService.create(task);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "更新定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_EDIT)
|
||||
@Idempotent(message = "请勿重复更新定时任务")
|
||||
@Log(value = "更新定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> update(@RequestBody ScheduledTaskEntity task) {
|
||||
dynamicTaskService.update(task);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/pause/{id}")
|
||||
@Operation(summary = "暂停定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_PAUSE)
|
||||
@Idempotent(message = "请勿重复暂停定时任务")
|
||||
@Log(value = "暂停定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> pause(@PathVariable Long id) {
|
||||
dynamicTaskService.pause(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/resume/{id}")
|
||||
@Operation(summary = "恢复定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_RESUME)
|
||||
@Idempotent(message = "请勿重复恢复定时任务")
|
||||
@Log(value = "恢复定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> resume(@PathVariable Long id) {
|
||||
dynamicTaskService.resume(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除定时任务")
|
||||
@SaCheckPermission(PermissionConstant.TASK_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除定时任务")
|
||||
@Log(value = "删除定时任务", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> delete(@PathVariable Long id) {
|
||||
dynamicTaskService.delete(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
125
src/main/java/com/test/demo/controller/UserController.java
Normal file
125
src/main/java/com/test/demo/controller/UserController.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package com.test.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.test.demo.common.annotation.Log;
|
||||
import com.test.demo.common.annotation.RateLimit;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.base.PageResult;
|
||||
import com.test.demo.common.constant.PermissionConstant;
|
||||
import com.test.demo.common.result.R;
|
||||
import com.test.demo.common.validate.ValidGroup;
|
||||
import com.test.demo.enums.LogType;
|
||||
import com.test.demo.bo.UserBo;
|
||||
import com.test.demo.bo.UserQueryBo;
|
||||
import com.test.demo.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 用户管理接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "用户管理")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@PostMapping("/page")
|
||||
@Operation(summary = "分页查询用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_PAGE)
|
||||
@Log(value = "分页查询用户", type = LogType.OPER)
|
||||
@RateLimit(count = 20, time = 60)
|
||||
public R<PageResult<UserVo>> page(@RequestBody(required = false) UserQueryBo query) {
|
||||
if (query == null) {
|
||||
query = new UserQueryBo();
|
||||
}
|
||||
return R.ok(userService.page(query));
|
||||
}
|
||||
|
||||
@PostMapping("/detail/{id}")
|
||||
@Operation(summary = "获取用户详情")
|
||||
@SaCheckPermission(PermissionConstant.USER_DETAIL)
|
||||
public R<UserVo> detail(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(userService.getDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@Operation(summary = "新增用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_ADD)
|
||||
@Idempotent(message = "请勿重复新增用户")
|
||||
@Log(value = "新增用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> add(@RequestBody @Validated(ValidGroup.Add.class) UserBo bo) {
|
||||
userService.add(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/edit")
|
||||
@Operation(summary = "编辑用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_EDIT)
|
||||
@Idempotent(message = "请勿重复编辑用户")
|
||||
@Log(value = "编辑用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> edit(@RequestBody @Validated(ValidGroup.Update.class) UserBo bo) {
|
||||
userService.edit(bo);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/remove/{id}")
|
||||
@Operation(summary = "删除用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_REMOVE)
|
||||
@Idempotent(message = "请勿重复删除用户")
|
||||
@Log(value = "删除用户", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> remove(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id) {
|
||||
userService.remove(id);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/reset-password/{id}")
|
||||
@Operation(summary = "重置密码")
|
||||
@SaCheckPermission(PermissionConstant.USER_RESET_PASSWORD)
|
||||
@Idempotent(message = "请勿重复重置密码")
|
||||
@Log(value = "重置密码", type = LogType.OPER, saveToDB = true)
|
||||
public R<Void> resetPassword(
|
||||
@Parameter(description = "用户ID", example = "1")
|
||||
@PathVariable Long id,
|
||||
@Parameter(description = "新密码", example = "123456")
|
||||
@RequestParam String newPassword) {
|
||||
userService.resetPassword(id, newPassword);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_IMPORT)
|
||||
@Idempotent(message = "请勿重复导入用户")
|
||||
@Log(value = "导入用户", type = LogType.IMPORT, saveToDB = true)
|
||||
public R<Void> importUsers(
|
||||
@Parameter(description = "用户导入Excel文件")
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
userService.importUsers(file);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/export")
|
||||
@Operation(summary = "导出用户")
|
||||
@SaCheckPermission(PermissionConstant.USER_EXPORT)
|
||||
@Log(value = "导出用户", type = LogType.EXPORT, saveToDB = true)
|
||||
public void exportUsers(@RequestBody(required = false) UserQueryBo query, HttpServletResponse response) throws Exception {
|
||||
if (query == null) {
|
||||
query = new UserQueryBo();
|
||||
}
|
||||
userService.exportUsers(query, response);
|
||||
}
|
||||
}
|
||||
33
src/main/java/com/test/demo/convert/UserConvert.java
Normal file
33
src/main/java/com/test/demo/convert/UserConvert.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.test.demo.convert;
|
||||
|
||||
import com.test.demo.bo.UserBo;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import com.test.demo.vo.UserVo;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户对象转换器(MapStruct编译时生成实现)
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserConvert {
|
||||
|
||||
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
|
||||
|
||||
@Mapping(target = "statusDesc", expression = "java(entity.getStatus() != null ? entity.getStatus().getDesc() : null)")
|
||||
@Mapping(target = "status", expression = "java(entity.getStatus() != null ? entity.getStatus().getValue() : null)")
|
||||
UserVo toVo(UserEntity entity);
|
||||
|
||||
List<UserVo> toVoList(List<UserEntity> list);
|
||||
|
||||
@Mapping(target = "id", ignore = true)
|
||||
@Mapping(target = "status", ignore = true)
|
||||
UserEntity toEntity(UserBo bo);
|
||||
|
||||
@Mapping(target = "status", ignore = true)
|
||||
void updateEntity(UserBo bo, @MappingTarget UserEntity entity);
|
||||
}
|
||||
74
src/main/java/com/test/demo/entity/EnumItemEntity.java
Normal file
74
src/main/java/com/test/demo/entity/EnumItemEntity.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 枚举/字典项实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_enum_item")
|
||||
@Schema(name = "EnumItemEntity", description = "枚举/字典项实体")
|
||||
public class EnumItemEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240752513025")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "字典值", example = "LOGIN")
|
||||
private String itemValue;
|
||||
|
||||
@Schema(description = "显示文本", example = "登录日志")
|
||||
private String itemLabel;
|
||||
|
||||
@Schema(description = "系统枚举常量名", example = "LOGIN")
|
||||
private String itemName;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否系统内置", example = "1")
|
||||
private Integer builtin;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许后台编辑", example = "1")
|
||||
private Integer editable;
|
||||
|
||||
@Schema(description = "是否允许后台删除", example = "0")
|
||||
private Integer deletable;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "标签颜色", example = "#409EFF")
|
||||
private String color;
|
||||
|
||||
@Schema(description = "标签类型", example = "success")
|
||||
private String tagType;
|
||||
|
||||
@Schema(description = "备注", example = "系统默认项")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "最近同步时间")
|
||||
private LocalDateTime lastSyncTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
71
src/main/java/com/test/demo/entity/EnumTypeEntity.java
Normal file
71
src/main/java/com/test/demo/entity/EnumTypeEntity.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_enum_type")
|
||||
@Schema(name = "EnumTypeEntity", description = "枚举/字典类型实体")
|
||||
public class EnumTypeEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "枚举编码", example = "LogType")
|
||||
private String enumCode;
|
||||
|
||||
@Schema(description = "枚举名称", example = "日志类型")
|
||||
private String enumName;
|
||||
|
||||
@Schema(description = "枚举描述", example = "日志类型枚举")
|
||||
private String enumDesc;
|
||||
|
||||
@Schema(description = "系统枚举完整类名", example = "com.test.demo.enums.LogType")
|
||||
private String className;
|
||||
|
||||
@Schema(description = "包路径", example = "com.test.demo.enums")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "值类型", example = "java.lang.String")
|
||||
private String valueType;
|
||||
|
||||
@Schema(description = "来源类型", example = "SYSTEM", allowableValues = {"SYSTEM", "CUSTOM"})
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "是否系统内置", example = "1")
|
||||
private Integer builtin;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "是否允许系统同步更新", example = "1")
|
||||
private Integer syncEnabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "系统内置枚举")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "最近同步时间")
|
||||
private LocalDateTime lastSyncTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
49
src/main/java/com/test/demo/entity/LoginLogEntity.java
Normal file
49
src/main/java/com/test/demo/entity/LoginLogEntity.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 登录日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_login_log")
|
||||
@Schema(name = "LoginLogEntity", description = "登录日志实体")
|
||||
public class LoginLogEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "日志类型", example = "LOGIN", allowableValues = {"LOGIN", "LOGOUT"})
|
||||
private String logType;
|
||||
|
||||
@Schema(description = "结果信息", example = "登录成功")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "登录IP", example = "127.0.0.1")
|
||||
private String ip;
|
||||
|
||||
@Schema(description = "User-Agent")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "请求URL", example = "/api/auth/login")
|
||||
private String requestUrl;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
50
src/main/java/com/test/demo/entity/MenuEntity.java
Normal file
50
src/main/java/com/test/demo/entity/MenuEntity.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 菜单权限实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_menu")
|
||||
@Schema(name = "MenuEntity", description = "菜单权限实体")
|
||||
public class MenuEntity extends BaseEntity {
|
||||
|
||||
@Schema(description = "父级ID", example = "0")
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "菜单类型", example = "MENU", allowableValues = {"MENU", "BUTTON"})
|
||||
private String menuType;
|
||||
|
||||
@Schema(description = "菜单名称", example = "用户管理")
|
||||
private String menuName;
|
||||
|
||||
@Schema(description = "路由路径", example = "/user")
|
||||
private String path;
|
||||
|
||||
@Schema(description = "组件路径", example = "system/user/index")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "权限标识", example = "system:user:page")
|
||||
private String permission;
|
||||
|
||||
@Schema(description = "图标", example = "User")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "是否显示", example = "1")
|
||||
private Integer visible;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
77
src/main/java/com/test/demo/entity/OperLogEntity.java
Normal file
77
src/main/java/com/test/demo/entity/OperLogEntity.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 操作日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_oper_log")
|
||||
@Schema(name = "OperLogEntity", description = "操作日志实体")
|
||||
public class OperLogEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID", example = "2033103240660238338")
|
||||
private Long id;
|
||||
|
||||
/** 操作说明 */
|
||||
@Schema(description = "操作说明", example = "新增用户")
|
||||
private String title;
|
||||
|
||||
/** 日志类型 */
|
||||
@Schema(description = "日志类型", example = "OPER")
|
||||
private String logType;
|
||||
|
||||
/** 请求方法(类名.方法名) */
|
||||
@Schema(description = "请求方法", example = "com.test.demo.controller.UserController.add")
|
||||
private String method;
|
||||
|
||||
/** HTTP方法 */
|
||||
@Schema(description = "HTTP方法", example = "POST")
|
||||
private String requestMethod;
|
||||
|
||||
/** 请求URL */
|
||||
@Schema(description = "请求URL", example = "/api/user")
|
||||
private String requestUrl;
|
||||
|
||||
/** 请求参数(脱敏后) */
|
||||
@Schema(description = "请求参数(脱敏后)")
|
||||
private String requestParam;
|
||||
|
||||
/** 返回结果(脱敏后) */
|
||||
@Schema(description = "返回结果(脱敏后)")
|
||||
private String responseResult;
|
||||
|
||||
/** 操作用户ID */
|
||||
@Schema(description = "操作用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
/** 操作用户名 */
|
||||
@Schema(description = "操作用户名", example = "admin")
|
||||
private String userName;
|
||||
|
||||
/** 操作IP */
|
||||
@Schema(description = "操作IP", example = "127.0.0.1")
|
||||
private String ip;
|
||||
|
||||
/** 操作状态(0失败 1成功) */
|
||||
@Schema(description = "操作状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
/** 错误信息 */
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
/** 耗时(ms) */
|
||||
@Schema(description = "耗时(ms)", example = "35")
|
||||
private Long costTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
32
src/main/java/com/test/demo/entity/RoleEntity.java
Normal file
32
src/main/java/com/test/demo/entity/RoleEntity.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_role")
|
||||
@Schema(name = "RoleEntity", description = "角色实体")
|
||||
public class RoleEntity extends BaseEntity {
|
||||
|
||||
@Schema(description = "角色编码", example = "ADMIN")
|
||||
private String roleCode;
|
||||
|
||||
@Schema(description = "角色名称", example = "系统管理员")
|
||||
private String roleName;
|
||||
|
||||
@Schema(description = "是否启用", example = "1")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "排序", example = "0")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "备注", example = "默认管理员角色")
|
||||
private String remark;
|
||||
}
|
||||
31
src/main/java/com/test/demo/entity/RoleMenuEntity.java
Normal file
31
src/main/java/com/test/demo/entity/RoleMenuEntity.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色菜单关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_role_menu")
|
||||
@Schema(name = "RoleMenuEntity", description = "角色菜单关联实体")
|
||||
public class RoleMenuEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "菜单ID", example = "1")
|
||||
private Long menuId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
45
src/main/java/com/test/demo/entity/ScheduledTaskEntity.java
Normal file
45
src/main/java/com/test/demo/entity/ScheduledTaskEntity.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 定时任务实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_scheduled_task")
|
||||
@Schema(name = "ScheduledTaskEntity", description = "定时任务实体")
|
||||
public class ScheduledTaskEntity extends BaseEntity {
|
||||
|
||||
/** 任务名称 */
|
||||
@Schema(description = "任务名称", example = "清理临时文件")
|
||||
private String taskName;
|
||||
|
||||
/** cron表达式 */
|
||||
@Schema(description = "Cron表达式", example = "0 0/5 * * * ?")
|
||||
private String cronExpression;
|
||||
|
||||
/** Bean名称 */
|
||||
@Schema(description = "Bean名称", example = "fileService")
|
||||
private String beanName;
|
||||
|
||||
/** 方法名 */
|
||||
@Schema(description = "方法名", example = "clearTempFiles")
|
||||
private String methodName;
|
||||
|
||||
/** 方法参数 */
|
||||
@Schema(description = "方法参数", example = "7")
|
||||
private String methodParams;
|
||||
|
||||
/** 状态(0暂停 1运行) */
|
||||
@Schema(description = "状态", example = "1", allowableValues = {"0", "1"})
|
||||
private Integer status;
|
||||
|
||||
/** 备注 */
|
||||
@Schema(description = "备注", example = "每5分钟执行一次")
|
||||
private String remark;
|
||||
}
|
||||
50
src/main/java/com/test/demo/entity/UserEntity.java
Normal file
50
src/main/java/com/test/demo/entity/UserEntity.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.test.demo.common.base.BaseEntity;
|
||||
import com.test.demo.enums.UserStatusEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user")
|
||||
@Schema(name = "UserEntity", description = "用户实体")
|
||||
public class UserEntity extends BaseEntity {
|
||||
|
||||
/** 用户名 */
|
||||
@Schema(description = "用户名", example = "admin")
|
||||
private String username;
|
||||
|
||||
/** SM3加密密码 */
|
||||
@Schema(description = "SM3加密密码")
|
||||
private String password;
|
||||
|
||||
/** 密码盐 */
|
||||
@Schema(description = "密码盐")
|
||||
private String salt;
|
||||
|
||||
/** 昵称 */
|
||||
@Schema(description = "昵称", example = "管理员")
|
||||
private String nickname;
|
||||
|
||||
/** 邮箱 */
|
||||
@Schema(description = "邮箱", example = "admin@example.com")
|
||||
private String email;
|
||||
|
||||
/** 手机号 */
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
private String phone;
|
||||
|
||||
/** 头像路径 */
|
||||
@Schema(description = "头像路径", example = "/avatar/admin.png")
|
||||
private String avatar;
|
||||
|
||||
/** 状态 */
|
||||
@Schema(description = "状态枚举")
|
||||
private UserStatusEnum status;
|
||||
}
|
||||
31
src/main/java/com/test/demo/entity/UserRoleEntity.java
Normal file
31
src/main/java/com/test/demo/entity/UserRoleEntity.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.test.demo.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("sys_user_role")
|
||||
@Schema(name = "UserRoleEntity", description = "用户角色关联实体")
|
||||
public class UserRoleEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户ID", example = "1")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色ID", example = "1")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
37
src/main/java/com/test/demo/enums/LogType.java
Normal file
37
src/main/java/com/test/demo/enums/LogType.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.test.demo.enums;
|
||||
|
||||
import com.test.demo.common.enums.EnumDesc;
|
||||
import com.test.demo.common.enums.IBaseEnum;
|
||||
|
||||
/**
|
||||
* 日志类型枚举
|
||||
*/
|
||||
@EnumDesc("日志类型枚举")
|
||||
public enum LogType implements IBaseEnum<String> {
|
||||
|
||||
LOGIN("LOGIN", "登录日志"),
|
||||
LOGOUT("LOGOUT", "登出日志"),
|
||||
OPER("OPER", "操作日志"),
|
||||
MAIL("MAIL", "邮件日志"),
|
||||
EXPORT("EXPORT", "导出日志"),
|
||||
IMPORT("IMPORT", "导入日志"),
|
||||
OTHER("OTHER", "其他日志");
|
||||
|
||||
private final String value;
|
||||
private final String desc;
|
||||
|
||||
LogType(String value, String desc) {
|
||||
this.value = value;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDesc() {
|
||||
return this.desc;
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/test/demo/enums/UserStatusEnum.java
Normal file
23
src/main/java/com/test/demo/enums/UserStatusEnum.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.test.demo.common.enums.EnumDesc;
|
||||
import com.test.demo.common.enums.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 用户状态枚举
|
||||
*/
|
||||
@EnumDesc("用户状态枚举")
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum UserStatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
DISABLED(0, "禁用"),
|
||||
NORMAL(1, "正常");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
private final String desc;
|
||||
}
|
||||
25
src/main/java/com/test/demo/filter/RepeatableReadFilter.java
Normal file
25
src/main/java/com/test/demo/filter/RepeatableReadFilter.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体过滤器
|
||||
*/
|
||||
public class RepeatableReadFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
String contentType = req.getContentType();
|
||||
// 仅对 JSON 请求体做包装
|
||||
if (contentType != null && contentType.contains("application/json")) {
|
||||
chain.doFilter(new RepeatableReadRequestWrapper(req), response);
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import jakarta.servlet.ReadListener;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* 可重复读取请求体的包装器(用于日志记录、签名校验等需要多次读取 body 的场景)
|
||||
*/
|
||||
public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final byte[] body;
|
||||
|
||||
public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
try (InputStream is = request.getInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, len);
|
||||
}
|
||||
this.body = baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener listener) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/test/demo/filter/XssFilter.java
Normal file
43
src/main/java/com/test/demo/filter/XssFilter.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* XSS 防护过滤器
|
||||
*/
|
||||
public class XssFilter implements Filter {
|
||||
|
||||
private List<String> excludes;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
String excludeStr = filterConfig.getInitParameter("excludes");
|
||||
if (StrUtil.isNotBlank(excludeStr)) {
|
||||
excludes = Arrays.asList(excludeStr.split(","));
|
||||
} else {
|
||||
excludes = List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
if (isExclude(req)) {
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
chain.doFilter(new XssRequestWrapper(req), response);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExclude(HttpServletRequest request) {
|
||||
String uri = request.getServletPath();
|
||||
return excludes.stream().anyMatch(uri::startsWith);
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/test/demo/filter/XssRequestWrapper.java
Normal file
40
src/main/java/com/test/demo/filter/XssRequestWrapper.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.test.demo.filter;
|
||||
|
||||
import cn.hutool.http.HtmlUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
|
||||
/**
|
||||
* XSS 请求包装器 - 对参数和请求头做 HTML 转义
|
||||
*/
|
||||
public class XssRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
public XssRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return value != null ? HtmlUtil.escape(value) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
String[] escaped = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
escaped[i] = HtmlUtil.escape(values[i]);
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return value != null ? HtmlUtil.escape(value) : null;
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/test/demo/handler/MyMetaObjectHandler.java
Normal file
25
src/main/java/com/test/demo/handler/MyMetaObjectHandler.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.test.demo.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus 自动填充处理器
|
||||
*/
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.test.demo.interceptor;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.test.demo.common.annotation.Idempotent;
|
||||
import com.test.demo.common.constant.CommonConstant;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 幂等性拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IdempotentInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Idempotent idempotent = handlerMethod.getMethodAnnotation(Idempotent.class);
|
||||
if (idempotent == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 构建幂等 key: token + url + 参数hash
|
||||
String token = request.getHeader(CommonConstant.HEADER_IDEMPOTENT_TOKEN);
|
||||
if (token == null) {
|
||||
token = request.getHeader("Authorization");
|
||||
}
|
||||
String url = request.getRequestURI();
|
||||
String queryString = request.getQueryString() != null ? request.getQueryString() : "";
|
||||
String key = RedisKeyConstant.IDEMPOTENT + DigestUtil.md5Hex(token + url + queryString);
|
||||
|
||||
Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, "1", idempotent.expireTime(), TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(absent)) {
|
||||
log.warn("幂等性拦截: key={}", key);
|
||||
throw new BizException(ResultCode.IDEMPOTENT_REJECT.getCode(), idempotent.message());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.test.demo.interceptor;
|
||||
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.test.demo.common.constant.CommonConstant;
|
||||
import com.test.demo.common.constant.RedisKeyConstant;
|
||||
import com.test.demo.common.exception.BizException;
|
||||
import com.test.demo.common.result.ResultCode;
|
||||
import com.test.demo.filter.RepeatableReadRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口签名/防重放拦截器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SignatureInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Value("${security.sign.secret:change-me-sign-secret}")
|
||||
private String signSecret;
|
||||
|
||||
@Value("${security.sign.expire-seconds:300}")
|
||||
private long expireSeconds;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String timestamp = request.getHeader(CommonConstant.HEADER_TIMESTAMP);
|
||||
String nonce = request.getHeader(CommonConstant.HEADER_NONCE);
|
||||
String sign = request.getHeader(CommonConstant.HEADER_SIGN);
|
||||
|
||||
// 缺少签名头则跳过(可根据需求改为拒绝)
|
||||
if (timestamp == null || nonce == null || sign == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 时间戳校验
|
||||
long ts = Long.parseLong(timestamp);
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
if (Math.abs(now - ts) > expireSeconds) {
|
||||
throw new BizException(ResultCode.REPLAY_ATTACK);
|
||||
}
|
||||
|
||||
// nonce 防重放
|
||||
String nonceKey = RedisKeyConstant.NONCE + nonce;
|
||||
Boolean absent = redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", expireSeconds, TimeUnit.SECONDS);
|
||||
if (Boolean.FALSE.equals(absent)) {
|
||||
throw new BizException(ResultCode.REPLAY_ATTACK);
|
||||
}
|
||||
|
||||
// 签名校验
|
||||
String body = "";
|
||||
if (request instanceof RepeatableReadRequestWrapper wrapper) {
|
||||
body = new String(wrapper.getBody(), StandardCharsets.UTF_8);
|
||||
}
|
||||
String rawSign = timestamp + nonce + body + signSecret;
|
||||
String expectedSign = DigestUtil.sha256Hex(rawSign);
|
||||
if (!expectedSign.equalsIgnoreCase(sign)) {
|
||||
throw new BizException(ResultCode.SIGNATURE_INVALID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/EnumItemMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/EnumItemMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.EnumItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 枚举/字典项 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface EnumItemMapper extends BaseMapper<EnumItemEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/EnumTypeMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/EnumTypeMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.EnumTypeEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 枚举/字典类型 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface EnumTypeMapper extends BaseMapper<EnumTypeEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/LoginLogMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/LoginLogMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.LoginLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 登录日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface LoginLogMapper extends BaseMapper<LoginLogEntity> {
|
||||
}
|
||||
48
src/main/java/com/test/demo/mapper/MenuMapper.java
Normal file
48
src/main/java/com/test/demo/mapper/MenuMapper.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.MenuEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 菜单 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface MenuMapper extends BaseMapper<MenuEntity> {
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT m.id, m.parent_id, m.menu_type, m.menu_name, m.path, m.component, m.permission,
|
||||
m.icon, m.visible, m.enabled, m.sort, m.remark, m.create_time, m.update_time, m.deleted
|
||||
FROM sys_menu m
|
||||
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
|
||||
INNER JOIN sys_role r ON r.id = rm.role_id
|
||||
WHERE ur.user_id = #{userId}
|
||||
AND m.enabled = 1
|
||||
AND m.visible = 1
|
||||
AND m.deleted = 0
|
||||
AND r.enabled = 1
|
||||
AND r.deleted = 0
|
||||
ORDER BY m.sort ASC, m.id ASC
|
||||
""")
|
||||
List<MenuEntity> selectMenusByUserId(Long userId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT m.permission
|
||||
FROM sys_menu m
|
||||
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
|
||||
INNER JOIN sys_role r ON r.id = rm.role_id
|
||||
WHERE ur.user_id = #{userId}
|
||||
AND m.enabled = 1
|
||||
AND m.deleted = 0
|
||||
AND r.enabled = 1
|
||||
AND r.deleted = 0
|
||||
AND m.permission IS NOT NULL
|
||||
AND m.permission <> ''
|
||||
""")
|
||||
List<String> selectPermissionsByUserId(Long userId);
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/OperLogMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/OperLogMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.OperLogEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 操作日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface OperLogMapper extends BaseMapper<OperLogEntity> {
|
||||
}
|
||||
23
src/main/java/com/test/demo/mapper/RoleMapper.java
Normal file
23
src/main/java/com/test/demo/mapper/RoleMapper.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.RoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<RoleEntity> {
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT r.role_code
|
||||
FROM sys_role r
|
||||
INNER JOIN sys_user_role ur ON ur.role_id = r.id
|
||||
WHERE ur.user_id = #{userId} AND r.enabled = 1 AND r.deleted = 0
|
||||
""")
|
||||
List<String> selectRoleCodesByUserId(Long userId);
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/RoleMenuMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/RoleMenuMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.RoleMenuEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色菜单 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMenuMapper extends BaseMapper<RoleMenuEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/ScheduledTaskMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/ScheduledTaskMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.ScheduledTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 定时任务 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ScheduledTaskMapper extends BaseMapper<ScheduledTaskEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/UserMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/UserMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.UserEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<UserEntity> {
|
||||
}
|
||||
12
src/main/java/com/test/demo/mapper/UserRoleMapper.java
Normal file
12
src/main/java/com/test/demo/mapper/UserRoleMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.test.demo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.test.demo.entity.UserRoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户角色 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserRoleMapper extends BaseMapper<UserRoleEntity> {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user