提交 280bfc17 作者: 方治民

合并分支 '3.0' 到 'beta'

3.0

查看合并请求 !14
流水线 #1993 已失败 于阶段
in 34 秒
...@@ -33,7 +33,7 @@ build-job: ...@@ -33,7 +33,7 @@ build-job:
before_script: before_script:
- chmod +x ./gradlew - chmod +x ./gradlew
script: script:
- ./gradlew app:assemble -Dskip-hooks - ./gradlew :app:assemble -Dskip-hooks
artifacts: artifacts:
# 配置构建结果过期时间 # 配置构建结果过期时间
expire_in: 1 day expire_in: 1 day
...@@ -54,12 +54,19 @@ deploy-job: ...@@ -54,12 +54,19 @@ deploy-job:
tags: tags:
- YR-CD - YR-CD
script: script:
# 容器名
- NAME=$CONTAINER_NAME-$CI_BUILD_REF_NAME
# 尝试停止并删除上一个容器
- id=$(docker ps -aqf name=$NAME) && [ "$id" ] && docker stop $id && docker rm -f $id || true
# 尝试删除镜像(先删除再构建,避免产生 <none> image)
- iid=$(docker images -aq $TAG) && [ "$iid" ] && docker rmi -f $iid || true
# 基于 Dockerfile 构建镜像 # 基于 Dockerfile 构建镜像
- docker build -t $TAG . - docker build -t $TAG .
# 尝试删除上一个容器 # 条件判断
- id=$(docker ps -aqf name=$CONTAINER_NAME) && [ "$id" ] && docker rm -f $id || true - PORT=$EXPOSE_PORT
- echo "Branch:" $CI_BUILD_REF_NAME "PORT:" $PORT "Container:" $NAME
# 在本地运行构建好的镜像 # 在本地运行构建好的镜像
- docker run -d --name $CONTAINER_NAME -p $EXPOSE_PORT:8081 $TAG - docker run -d --name $NAME -p $PORT:8081 -e TZ="Asia/Shanghai" $TAG
variables: variables:
# 设置镜像 tag,使用 git tag 标识作为镜像 tag # 设置镜像 tag,使用 git tag 标识作为镜像 tag
TAG: $REGISTRY_REMOTE/basic/$CONTAINER_NAME:$CI_BUILD_REF_NAME TAG: $REGISTRY_REMOTE/basic/$CONTAINER_NAME:$CI_BUILD_REF_NAME
# 指定基础镜像,在其上进行定制 # 指定基础镜像,在其上进行定制
FROM localhost:18500/jdk-17 FROM localhost:18500/jdk-17-ffmpeg
# 维护者信息 # 维护者信息
MAINTAINER ifzm <fangzhimin@yiring.com> MAINTAINER ifzm <fangzhimin@yiring.com>
...@@ -16,6 +16,9 @@ COPY app/build/libs/app-0.0.1-SNAPSHOT.jar app.jar ...@@ -16,6 +16,9 @@ COPY app/build/libs/app-0.0.1-SNAPSHOT.jar app.jar
# RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。 # RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。
# RUN bash -c "touch /app.jar" # RUN bash -c "touch /app.jar"
# 设置时区,解决时区问题
RUN echo "Asia/Shanghai" > /etc/timezone
# 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务 # 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
EXPOSE 8081 EXPOSE 8081
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
- [x] 完成项目构建,开发文档编写 - [x] 完成项目构建,开发文档编写
- [x] [conventional-changelog](https://www.cnblogs.com/mengfangui/p/12634845.html) - [x] [conventional-changelog](https://www.cnblogs.com/mengfangui/p/12634845.html)
- [x] 用户及权限模块(目录/菜单/按钮),预览初始化权限配置 [SQL 脚本](./basic-auth/src/main/resources/init-test-mysql.sql) - [x] 用户及权限模块(目录/菜单/按钮),预览初始化权限配置 [SQL 脚本](./basic-auth/src/main/resources/init-test-mysql.sql)
- [x] 通用文件上传模块 - [x] 通用文件上传模块,支持对图片/PDF/MP3/MP4 等文件进行预处理
- [x] WebSocket 模块
- [ ] 通用字典管理模块 - [ ] 通用字典管理模块
- [ ] XXL-JOB 定时任务模块 - [ ] XXL-JOB 定时任务模块
group = 'com.yiring' group = 'com.yiring'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
processResources {
filesMatching('application.yml') {
expand(project.properties)
}
}
bootJar { bootJar {
enabled = true enabled = true
} }
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 💬 Mock/Test Env // 💬 Mock/Test Env
runtimeOnly 'com.h2database:h2' runtimeOnly 'com.h2database:h2'
// 💬 Prod/Dev Env // 💬 Prod/Dev Env
...@@ -22,33 +28,44 @@ dependencies { ...@@ -22,33 +28,44 @@ dependencies {
implementation project(":basic-common:core") implementation project(":basic-common:core")
implementation project(":basic-common:util") implementation project(":basic-common:util")
// Optional: I18n 消息, 包括参数校验、失败的请求提示等
implementation project(":basic-common:i18n")
// Optional: Redis // Optional: Redis
implementation project(":basic-common:redis") implementation project(":basic-common:redis")
// Optional: Doc // Optional: Doc
implementation project(":basic-common:doc") implementation project(":basic-common:doc")
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// Optional: Auth // Optional: Auth
implementation project(":basic-auth") implementation project(":basic-auth")
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}" implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
// Optional: Dict - 数据字典
implementation project(":basic-dict")
// Optional: WebSocket && STOMP 依赖 Auth + Redis 模块
implementation project(":basic-websocket")
// Optional: Minio S3 // Optional: Minio S3
implementation project(":basic-common:minio") implementation project(":basic-common:minio")
// FIX: minio dep // FIX: minio dep
implementation "io.minio:minio:${minioVersion}"
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
// Optional: 扩展实现在文件上传时对文件进行预处理,依赖 Minio 模块
// Optional: MyBatis Plus // https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "com.baomidou:mybatis-plus-boot-starter:${mybatisPlusVersion}" implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool // hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-extra:${hutoolVersion}" implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types // https://github.com/vladmihalcea/hypersistence-utils
// hibernate-types-55 implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app; package com.yiring.app;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@MapperScan(basePackages = Application.BASE_PACKAGES + ".app.mapper")
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@EntityScan( @EntityScan(
basePackageClasses = { Application.class, Jsr310JpaConverters.class }, basePackageClasses = { Application.class, Jsr310JpaConverters.class },
basePackages = Application.BASE_PACKAGES basePackages = Application.BASE_PACKAGES
) )
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES) @SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES)
public class Application { public class Application {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app.config; package com.yiring.app.config;
import cn.dev33.satoken.exception.NotLoginException; import com.yiring.common.core.I18n;
import com.yiring.app.constant.Code;
import com.yiring.app.exception.CodeException;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.FailStatusException; import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException; import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException; import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** /**
* 全局错误处理 * 全局错误处理
...@@ -26,83 +19,12 @@ import org.springframework.web.bind.annotation.ResponseBody; ...@@ -26,83 +19,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
* 2017年11月30日 上午11:36:31 * 2017年11月30日 上午11:36:31
*/ */
@Slf4j @Slf4j
@ControllerAdvice @Order
@ResponseBody @RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/** final I18n i18n;
* 参数校验异常
*
* @param e 异常信息
* @return 统一的校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(
value = { BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
)
public Result<String> bindErrorHandler(Exception e) {
String error = "未知参数校验错误";
if (e instanceof ConstraintViolationException) {
error = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
} else {
BindingResult result = null;
if (e instanceof MethodArgumentNotValidException) {
result = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
result = ((BindException) e).getBindingResult();
}
if (result != null) {
error = result.getAllErrors().iterator().next().getDefaultMessage();
}
}
return Result.no(Status.EXPECTATION_FAILED, error);
}
/**
* 不支持的HttpMethod异常
*
* @param e 异常信息
* @return 异常信息反馈 {@link Status#METHOD_NOT_ALLOWED
*/
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public Result<String> httpRequestMethodNotSupportedErrorHandler(Exception e) {
return Result.no(Status.METHOD_NOT_ALLOWED, e.getMessage());
}
/**
* 未登录异常(鉴权失败)
*
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(value = NotLoginException.class)
public Result<String> notLoginErrorHandler() {
return Result.no(Status.UNAUTHORIZED);
}
/**
* 自定义业务异常
*/
@ExceptionHandler(value = CodeException.class)
public Result<String> customCodeExceptionHandler(CodeException e) {
Code code = e.getCode();
return Result.no(Status.BAD_REQUEST, code.value(), code.reason(), null);
}
/**
* 失败状态异常
*/
@ExceptionHandler(value = FailStatusException.class)
public Result<String> failStatusExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus());
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler(value = ClientAbortException.class)
public void clientAbortExceptionHandler() {}
/** /**
* 其他异常 * 其他异常
...@@ -110,9 +32,9 @@ public class GlobalExceptionHandler { ...@@ -110,9 +32,9 @@ public class GlobalExceptionHandler {
* @param e 异常信息 * @param e 异常信息
* @return 统一的500异常信息 {@link Status#INTERNAL_SERVER_ERROR * @return 统一的500异常信息 {@link Status#INTERNAL_SERVER_ERROR
*/ */
@ExceptionHandler(value = Exception.class) @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public Result<String> defaultErrorHandler(Exception e, HttpServletResponse response) { @ExceptionHandler(Exception.class)
response.setStatus(Status.INTERNAL_SERVER_ERROR.value()); public Result<String> defaultErrorHandler(Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.no(Status.INTERNAL_SERVER_ERROR, e); return Result.no(Status.INTERNAL_SERVER_ERROR, e);
} }
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.constant;
import io.swagger.annotations.ApiModel;
/**
* 业务状态码
* eg: <code>throw new CodeException(Code.FAIL)</code>
*
* @author Jim
* @version 0.1
* 2022/3/25 9:23
*/
@SuppressWarnings({ "unused" })
@ApiModel("业务状态码")
public enum Code {
SUCCESS(0, "成功"),
// TODO: 扩展业务状态
// eg:
// 10001: 用户被禁止登录
FAIL(10000, "测试错误");
private final int value;
private final String reasonPhrase;
Code(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
/**
* Return the integer value of this status code.
*/
public int value() {
return this.value;
}
/**
* Return the reason phrase of this status code.
*/
public String reason() {
return this.reasonPhrase;
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.app.domain; package com.yiring.app.domain.user;
import com.baomidou.mybatisplus.annotation.TableName; import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
/** /**
* 测试表 * 用户扩展
* *
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/4/15 18:34 * 2022/7/13 10:59
*/ */
@Getter @Getter
...@@ -29,21 +28,28 @@ import org.hibernate.annotations.Comment; ...@@ -29,21 +28,28 @@ import org.hibernate.annotations.Comment;
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Entity @Entity
@TableName("TEST_TABLE") @Table(name = "SYS_USER_EXTENSION", uniqueConstraints = @UniqueConstraint(columnNames = "user_id"))
@Table(name = "TEST_TABLE") @Comment("用户扩展表")
@Comment("测试表") public class UserExtension extends BasicEntity implements Serializable {
public class TestTable implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -6168070383092874608L; private static final long serialVersionUID = -1157047754883351972L;
@Comment("主键") @Comment("用户")
@Id @OneToOne
String id; @JoinColumn(nullable = false, name = "user_id")
User user;
@Comment("姓名") @Comment("性别")
String name; Integer gender;
@Comment("年龄") @Comment("年龄")
Integer age; Integer age;
@Comment("简介")
String introduction;
public UserExtension(User user) {
this.user = user;
}
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.domain.user;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2022/7/13 11:11
*/
@Repository
public interface UserExtensionRepository extends JpaRepository<UserExtension, Serializable> {}
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yiring.app.domain.TestTable;
/**
* @author Jim
* @version 0.1
* 2022/4/15 17:25
*/
public interface TestTableMapper extends BaseMapper<TestTable> {}
/**
* @author Jim
* @version 0.1
* 2022/4/5 16:57
*/
package com.yiring.app.mapper;
// MyBatis Mapper/XML 目录
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.service.upload;
import cn.hutool.core.io.FileUtil;
import com.yiring.common.config.EnvConfig;
import com.yiring.common.core.Minio;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.Commons;
import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegFormat;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* @author Jim
* @version 0.1
* 2022/9/23 16:44
*/
@Slf4j
@Primary
@Component
@RequiredArgsConstructor
public class UploadProcessServiceImpl implements UploadProcessService {
final Minio minio;
final EnvConfig env;
@SneakyThrows
@Override
public String handle(String object, MultipartFile file) {
String suffix = FileUtil.getSuffix(object);
// Image: 在文件名上追加图片物理像素
if (isSupportiveImage(suffix)) {
object = handleImage(object, file);
}
// PDF: 在文件名上追加页数,同时在同目录生成 PDF 每一页的图片
if (isPdf(suffix)) {
object = handlePdf(object, file);
}
// Video/Audio: 在文件名上追加时长,视频生成封面图
if (isSupportiveMedia(suffix)) {
object = handleMedia(object, suffix, file);
}
return object;
}
@SneakyThrows
public String handleImage(String object, MultipartFile file) {
@Cleanup
InputStream is = file.getInputStream();
BufferedImage image = ImageIO.read(is);
return fillSuffix(object, image.getWidth() + "x" + image.getHeight());
}
@SneakyThrows
public String handlePdf(String object, MultipartFile file) {
@Cleanup
InputStream is = file.getInputStream();
@Cleanup
PDDocument doc = PDDocument.load(is);
int pages = doc.getNumberOfPages();
// 构建具有 PDF 页数标记的存储地址
String filepath = fillSuffix(object, "P" + pages);
// 将 PDF 解析成图片上传
String format = "jpg";
PDFRenderer renderer = new PDFRenderer(doc);
for (int i = 0; i < pages; i++) {
@Cleanup
ByteArrayOutputStream os = new ByteArrayOutputStream();
BufferedImage image = renderer.renderImageWithDPI(i, 144);
ImageIO.write(image, format, os);
@Cleanup
InputStream io = new ByteArrayInputStream(os.toByteArray());
minio.putObject(io, MediaType.IMAGE_JPEG_VALUE, filepath + "." + (i + 1) + "." + format);
}
return filepath;
}
@SneakyThrows
public String handleMedia(String object, String suffix, MultipartFile file) {
// 判断是否配置 ffmpeg 环境
FFmpeg ffmpeg = new FFmpeg();
FFprobe ffprobe = new FFprobe();
try {
if (!ffmpeg.isFFmpeg() || !ffprobe.isFFprobe()) {
return object;
}
} catch (Exception e) {
log.warn(e.getMessage());
return object;
}
// 将上传的文件转存一份到本地临时目录
Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename());
FileUtil.mkParentDirs(tempFile);
file.transferTo(tempFile);
// 解析媒体文件
FFmpegProbeResult probeResult = ffprobe.probe(tempFile.toString());
FFmpegFormat format = probeResult.getFormat();
// 构建具有时长(秒)标记的存储地址
long sec = (long) format.duration;
String filepath = fillSuffix(object, "T" + sec);
// 视频截取首帧可见画面作为封面
if (isSupportiveVideo(suffix)) {
// 大视频文件切片上传(> 5s)
if (sec > 5) {
filepath = handleVideoToHls(filepath, tempFile, ffmpeg, ffprobe);
}
// 使用 ffmpeg 截取视频首帧图片
Path path = Paths.get(tempFile.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
handleVideoScreenshot(tempFile.toString(), path.toString(), filepath, ffmpeg, ffprobe);
}
// 删除为本次上传进行本地处理所创建的整个临时文件夹目录
FileUtil.del(tempFile.getParent());
return filepath;
}
@SneakyThrows
public void handleVideoScreenshot(String in, String out, String object, FFmpeg ffmpeg, FFprobe ffprobe) {
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(in)
.overrideOutputFiles(true)
.addOutput(out)
.setFrames(1)
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
// 判断封面图是否生成
Path path = Paths.get(out);
if (Files.exists(path)) {
// 上传封面图
minio.putObject(path.toFile(), getObjectFolder(object));
}
}
@SneakyThrows
public String handleVideoToHls(String object, Path path, FFmpeg ffmpeg, FFprobe ffprobe) {
String originName = FileUtil.getName(object);
String objectFolder = object.replace("/" + originName, "");
String targetName = originName.replaceAll("^(.*)\\." + FileUtil.getSuffix(originName) + "$", "$1.m3u8");
// 使用 ffmpeg 将视频文件转换成 hls 切片文件 m3u8+ts
// "-vsync", "2", "-c:v", "copy", "-c:a", "copy", "-tune", "fastdecode", "-hls_wrap", "0", "-hls_time", "10", "-hls_list_size", "0", "-threads", "12"
final FFmpegProbeResult probe = ffprobe.probe(path.toString());
Path tempFile = Paths.get(path.getParent().toString(), targetName);
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(path.toString())
.overrideOutputFiles(true)
.addOutput(tempFile.toString())
.setFormat(probe.getFormat().format_name)
.setStrict(FFmpegBuilder.Strict.STRICT)
.setFormat("hls")
.setPreset("ultrafast")
.addExtraArgs(
"-vsync",
"2",
"-c:v",
"copy",
"-c:a",
"copy",
"-tune",
"fastdecode",
"-hls_wrap",
"0",
"-hls_time",
"5",
"-hls_list_size",
"0",
"-hls_segment_filename",
tempFile.toString().replaceAll("^(.*)\\.m3u8$", "$1-%d.ts"),
"-threads",
"8"
)
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
if (Files.exists(tempFile)) {
Pattern pattern = Pattern.compile("^.*\\.ts$");
// 解析 m3u8 文件
List<String> lines = FileUtil.readLines(tempFile.toString(), StandardCharsets.UTF_8);
// 获取 ts 切片文件
List<String> tss = lines.stream().filter(line -> pattern.matcher(line).matches()).toList();
// 上传 ts 切片文件
for (String ts : tss) {
Path temp = Paths.get(tempFile.getParent().toString(), ts);
minio.putObject(temp.toFile(), objectFolder);
}
// 上传 m3u8 索引文件
ObjectWriteResponse objectWriteResponse = minio.putObject(tempFile.toFile(), objectFolder);
object = objectWriteResponse.object();
// 手动 GC 一次
System.gc();
}
return object;
}
public static boolean isSupportiveMedia(String suffix) {
return isSupportiveVideo(suffix) || isSupportiveAudio(suffix);
}
public static boolean isSupportiveVideo(String suffix) {
return List.of("mp4", "flv", "avi", "rmvb", "rm", "wmv", "mkv", "mpg", "mpeg").contains(suffix.toLowerCase());
}
public static boolean isSupportiveAudio(String suffix) {
return List.of("mp3", "wav").contains(suffix.toLowerCase());
}
public static boolean isSupportiveImage(String suffix) {
return List.of("png", "jpg", "webp", "gif", "tif", "svg", "bmp").contains(suffix.toLowerCase());
}
public static boolean isPdf(String suffix) {
return "pdf".equalsIgnoreCase(suffix);
}
public static String fillSuffix(String object, String fill) {
String suffix = FileUtil.getSuffix(object);
String regex = "^(.*)\\." + suffix + "$";
return object.replaceAll(regex, "$1." + fill + "." + suffix);
}
public static String getObjectFolder(String object) {
String sourceName = FileUtil.getName(object);
return object.replace("/" + sourceName, "");
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.vo.user;
import com.yiring.common.jackson.MappingSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
/**
* @author Jim
* @version 0.1
* 2022/7/13 11:36
*/
@Schema(name = "UserExtensionVo", description = "用户扩展信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserExtensionVo implements Serializable {
@Serial
private static final long serialVersionUID = -2251567849918281906L;
@Schema(description = "性别", example = "男", allowableValues = { "男", "女" }, type = "string")
@MappingSerialize(mapping = "0:女,1:男")
Integer gender;
@Schema(description = "年龄", example = "18")
Integer age;
@Schema(description = "简介", example = "Hi")
String introduction;
}
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app.web.example; package com.yiring.app.web.example;
import cn.hutool.extra.spring.SpringUtil; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckSafe;
import cn.dev33.satoken.annotation.SaIgnore;
import com.github.xiaoymin.knife4j.annotations.ApiSupport; import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.app.constant.Code; import com.yiring.app.domain.user.UserExtension;
import com.yiring.app.domain.TestTable; import com.yiring.app.domain.user.UserExtensionRepository;
import com.yiring.app.exception.CodeException; import com.yiring.app.vo.user.UserExtensionVo;
import com.yiring.app.mapper.TestTableMapper; import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.service.FileManageService;
import com.yiring.common.util.Commons;
import com.yiring.common.util.FileUtils; import com.yiring.common.util.FileUtils;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletResponse; import java.util.Optional;
import javax.validation.Valid; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.Example;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 示例接口 * 示例接口
*
* @author Jim * @author Jim
*/ */
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = 0) @ApiSupport(order = 0)
@Api(tags = "示例", description = "Example") @Tag(name = "示例", description = "Example")
@RequestMapping("/example/") @RequestMapping("/example/")
@RestController @RestController
@RequiredArgsConstructor
public class ExampleController { public class ExampleController {
String text = "😎 Hello World"; final I18n i18n;
final Auths auths;
final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
@Operation(summary = "Hello World")
@GetMapping @GetMapping
public Result<String> hello() { public Result<String> hello() {
return Result.ok(text); return Result.ok("example.hello");
} }
/** /**
* 测试失败自定义状态信息输出 * 测试失败自定义状态信息输出
*/ */
@Operation(summary = "测试失败")
@GetMapping("fail") @GetMapping("fail")
public Result<String> fail() { public Result<String> fail() {
throw new CodeException(Code.FAIL); throw BusinessException.i18n("Code.1");
} }
@SaCheckLogin
@Operation(summary = "分页条件查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<String>> page(@Valid PageParam pageParam) { public Result<PageVo<String>> page(@ParameterObject @Validated PageParam param) {
log.info("PageParam: {}", pageParam); log.debug("PageParam: {}", param);
String text = i18n.get("example.hello");
List<String> data = Arrays.asList(text.split(" ")); List<String> data = Arrays.asList(text.split(" "));
PageVo<String> vo = PageVo.build(data, data.size()); PageVo<String> vo = PageVo.build(data, data.size());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @Operation(summary = "JSON 传参")
@GetMapping("download") @PostMapping("json")
public void download(HttpServletResponse response) throws IOException { public Result<PageVo<String>> json(
@RequestBody(required = false) @Validated(Group.Optional.class) PageParam param
) {
return page(param);
}
@SaIgnore
@DownloadResponse
@SneakyThrows(IOException.class)
@Operation(summary = "文件下载")
@GetMapping(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void download(HttpServletResponse response) {
ClassPathResource resource = new ClassPathResource("static/cat.jpg"); ClassPathResource resource = new ClassPathResource("static/cat.jpg");
FileUtils.download(response, resource.getFile()); FileUtils.download(response, resource.getFile());
} }
@ApiOperation("测试 MyBatis Plus 查询") @Operation(summary = "文件上传")
@GetMapping("test") @PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> test() { public Result<String> upload(
List<TestTable> tests = SpringUtil.getBean(TestTableMapper.class).selectList(null); @Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file,
return Result.ok(tests.stream().map(TestTable::getName).reduce((a, b) -> a + "," + b).orElse("")); @ParameterObject @Validated IdParam param
) {
log.info("upload params: {}", param);
try {
String link = fileManageService.upload(file);
return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
}
}
@SaCheckSafe
@SaCheckLogin
@Operation(summary = "查询用户属性")
@GetMapping("findUserExtensionInfo")
public Result<UserExtensionVo> findUserExtensionInfo() {
User user = auths.getLoginUser();
Optional<UserExtension> optional = userExtensionRepository.findOne(Example.of(new UserExtension(user)));
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
UserExtension ext = optional.get();
UserExtensionVo vo = Commons.transform(ext, UserExtensionVo.class);
return Result.ok(vo);
} }
} }
# 环境变量 # 环境变量
env: env:
host: 192.168.0.156 host: 192.168.0.156
prod: false
extra:
username: admin
password: 123456
# ----------------------------------------------
spring: spring:
datasource: datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai url: jdbc:postgresql://${env.host}:5432/basic_app
username: root username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true open-in-view: true
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: false show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
redis: # https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
database: 5 schema_update:
host: ${env.host} unique_constraint_strategy: RECREATE_QUIETLY
port: 6379 # https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827
# Optional: MyBatis Plus jakarta:
mybatis-plus: persistence:
global-config: sharedCache:
banner: false mode: UNSPECIFIED
data:
redis:
database: 5
port: 6379
host: ${env.host}
password: ${env.extra.password}
# ----------------------------------------------
# ----------------------------------------------
# Spring Doc
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: false
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: false language: zh_cn
enableOpenApi: true
enableDebug: true enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio # minio
minio: minio:
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: fatal org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# ----------------------------------------------
...@@ -8,8 +8,16 @@ spring: ...@@ -8,8 +8,16 @@ spring:
hibernate: hibernate:
# 关闭 hibernate-types banner 日志信息 # 关闭 hibernate-types banner 日志信息
types.print.banner: false types.print.banner: false
data:
redis:
repositories:
enabled: false
logging: logging:
level: level:
# 关闭接口扫描 CachingOperationNameGenerator 日志 # 关闭接口扫描 CachingOperationNameGenerator 日志
springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator: WARN springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator: WARN
# 关闭接口扫描 ApiListingReferenceScanner 日志
springfox.documentation.spring.web.scanners.ApiListingReferenceScanner: WARN
# https://github.com/spring-projects/spring-framework/issues/29612
org.springframework.core.LocalVariableTableParameterNameDiscoverer: ERROR
# 环境变量 # 环境变量
env: env:
host: 127.0.0.1 host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring: spring:
datasource: datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.MySQL8Dialect
open-in-view: true open-in-view: true
...@@ -20,33 +24,30 @@ spring: ...@@ -20,33 +24,30 @@ spring:
database: 5 database: 5
host: ${env.host} host: ${env.host}
port: 6379 port: 6379
password: 123456 password: ${env.extra.password}
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: false enableOpenApi: false
enableDebug: true enableDebug: true
# minio # minio
minio: minio:
access-key: minioadmin access-key: ${env.extra.username}
secret-key: minioadmin secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# 环境变量 # 环境变量
env: env:
host: 192.168.0.156 # host: 192.168.0.156
host: 127.0.0.1
prod: false
extra:
# username: admin
password: 123456
username: postgres
ffmpeg:
path: D:\Environments\FFmpeg\bin
# ----------------------------------------------
spring: spring:
datasource: datasource:
url: jdbc:postgresql://${env.host}:5432/basic_app url: jdbc:postgresql://${env.host}:5432/basic_app
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.PostgreSQL10Dialect database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true open-in-view: true
hibernate: hibernate:
ddl-auto: update ddl-auto: update
...@@ -16,37 +25,60 @@ spring: ...@@ -16,37 +25,60 @@ spring:
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
redis: # https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
database: 5 schema_update:
host: ${env.host} unique_constraint_strategy: RECREATE_QUIETLY
port: 6379 # https://github.com/spring-projects/spring-data-jpa/issues/2717
password: 123456 # https://hibernate.atlassian.net/browse/HHH-15827
jakarta:
# Optional: MyBatis Plus persistence:
mybatis-plus: sharedCache:
global-config: mode: UNSPECIFIED
banner: false data:
redis:
database: 5
port: 6379
host: ${env.host}
password: ${env.extra.password}
# ----------------------------------------------
# ----------------------------------------------
# Spring Doc
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: false
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
language: zh_cn
enableOpenApi: true enableOpenApi: true
enableDebug: true enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio # minio
minio: minio:
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# ----------------------------------------------
# 环境变量 # 环境变量
env: env:
host: 127.0.0.1 host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring: spring:
datasource: datasource:
...@@ -20,34 +24,30 @@ spring: ...@@ -20,34 +24,30 @@ spring:
database: 5 database: 5
host: ${env.host} host: ${env.host}
port: 6379 port: 6379
password: 123456 password: ${env.extra.password}
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: true enableOpenApi: true
enableDebug: true enableDebug: true
# minio # minio
minio: minio:
access-key: minioadmin access-key: ${env.extra.username}
secret-key: minioadmin secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
app:
version: ${version}
server: server:
port: 8081 port: 8081
servlet: servlet:
context-path: /api context-path: /api
tomcat:
max-http-form-post-size: 20MB
spring: spring:
servlet:
# 文件上传大小限制
multipart:
max-file-size: 10MB
max-request-size: 30MB
application: application:
name: "basic-api-app" name: "basic-api-app"
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1048MB
profiles: profiles:
include: auth, conf-patch include: auth, conf-patch
active: dev-postgresql active: dev-postgresql
......
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
jakarta.validation.constraints.AssertFalse.message=\u53EA\u80FD\u4E3Afalse
jakarta.validation.constraints.AssertTrue.message=\u53EA\u80FD\u4E3Atrue
jakarta.validation.constraints.DecimalMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
jakarta.validation.constraints.DecimalMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
jakarta.validation.constraints.Digits.message=\u6570\u5B57\u7684\u503C\u8D85\u51FA\u4E86\u5141\u8BB8\u8303\u56F4(\u53EA\u5141\u8BB8\u5728{integer}\u4F4D\u6574\u6570\u548C{fraction}\u4F4D\u5C0F\u6570\u8303\u56F4\u5185)
jakarta.validation.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
jakarta.validation.constraints.Future.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4
jakarta.validation.constraints.FutureOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u6216\u73B0\u5728\u7684\u65F6\u95F4
jakarta.validation.constraints.Max.message=\u6700\u5927\u4E0D\u80FD\u8D85\u8FC7{value}
jakarta.validation.constraints.Min.message=\u6700\u5C0F\u4E0D\u80FD\u5C0F\u4E8E{value}
jakarta.validation.constraints.Negative.message=\u5FC5\u987B\u662F\u8D1F\u6570
jakarta.validation.constraints.NegativeOrZero.message=\u5FC5\u987B\u662F\u8D1F\u6570\u6216\u96F6
jakarta.validation.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
jakarta.validation.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
jakarta.validation.constraints.NotNull.message=\u4E0D\u80FD\u4E3Anull
jakarta.validation.constraints.Null.message=\u5FC5\u987B\u4E3Anull
jakarta.validation.constraints.Past.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65F6\u95F4
jakarta.validation.constraints.PastOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u6216\u73B0\u5728\u7684\u65F6\u95F4
jakarta.validation.constraints.Pattern.message=\u9700\u8981\u5339\u914D\u6B63\u5219\u8868\u8FBE\u5F0F"{regexp}"
jakarta.validation.constraints.Positive.message=\u5FC5\u987B\u662F\u6B63\u6570
jakarta.validation.constraints.PositiveOrZero.message=\u5FC5\u987B\u662F\u6B63\u6570\u6216\u96F6
jakarta.validation.constraints.Size.message=\u4E2A\u6570\u5FC5\u987B\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.CreditCardNumber.message=\u4E0D\u5408\u6CD5\u7684\u4FE1\u7528\u5361\u53F7\u7801
org.hibernate.validator.constraints.Currency.message=\u4E0D\u5408\u6CD5\u7684\u8D27\u5E01 (\u5FC5\u987B\u662F{value}\u5176\u4E2D\u4E4B\u4E00)
org.hibernate.validator.constraints.EAN.message=\u4E0D\u5408\u6CD5\u7684{type}\u6761\u5F62\u7801
org.hibernate.validator.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
org.hibernate.validator.constraints.Length.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.CodePointLength.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.LuhnCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, Luhn\u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod10Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod11Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2111\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.ModCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, {modType}\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.ParametersScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.Range.message=\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.ScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.URL.message=\u9700\u8981\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL
org.hibernate.validator.constraints.time.DurationMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
org.hibernate.validator.constraints.time.DurationMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
/* (C) 2022 YiRing, Inc. */
package com.yiring;
import com.yiring.app.domain.TestTable;
import com.yiring.app.mapper.TestTableMapper;
import java.util.List;
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
/**
* @author Jim
* @version 0.1
* 2022/4/15 17:25
*/
@SpringBootTest
public class MapperSampleTest {
@Resource
TestTableMapper testTableMapper;
@Test
public void testSelect() {
List<TestTable> tests = testTableMapper.selectList(null);
Assert.isTrue(tests.size() > 0, "查询失败");
}
}
...@@ -9,20 +9,25 @@ dependencies { ...@@ -9,20 +9,25 @@ dependencies {
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar']) implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// sa-token // sa-token
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}" implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
implementation "cn.dev33:sa-token-dao-redis-jackson:${saTokenVersion}" implementation "cn.dev33:sa-token-dao-redis-jackson:${saTokenVersion}"
// Sa-Token 整合 Redis (使用 jackson 序列化方式)
implementation 'org.apache.commons:commons-pool2'
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool-core // hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types // https://github.com/vladmihalcea/hypersistence-utils
// hibernate-types-55 // hypersistence-utils-hibernate-60
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}" implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
} }
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.config;
import cn.dev33.satoken.exception.*;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 授权异常拦截处理
*
* @author Jim
* @version 0.1
* 2023/1/12 14:06
*/
@Slf4j
@Order(1)
@RestControllerAdvice
@RequiredArgsConstructor
public class AuthExceptionHandler {
/**
* 未登录异常(鉴权失败)
*
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(NotLoginException.class)
public Result<String> unauthorizedErrorHandler() {
return Result.no(Status.UNAUTHORIZED);
}
/**
* 1. 二级认证失败异常
* 2. 角色条件不满足
* 3. 权限条件不满足
* 4. HTTP Basic 验证不通过
* 5. 用户被禁止访问该服务
* 6. API 被禁用
*
* @return 异常信息反馈 {@link Status#FORBIDDEN
*/
@ExceptionHandler(
{
// https://sa-token.dev33.cn/doc.html#/up/safe-auth
NotSafeException.class,
// https://sa-token.dev33.cn/doc.html#/use/at-check
NotRoleException.class,
NotPermissionException.class,
// https://sa-token.dev33.cn/doc.html#/up/basic-auth
NotBasicAuthException.class,
// https://sa-token.dev33.cn/doc.html#/up/disable
DisableServiceException.class,
ApiDisabledException.class,
}
)
public Result<String> forbiddenHandler(Exception e) {
if (e instanceof NotSafeException) {
return Result.no(Status.FORBIDDEN, "Code.10000");
}
if (e instanceof NotRoleException) {
return Result.no(Status.FORBIDDEN, "Code.10001");
}
if (e instanceof NotPermissionException) {
return Result.no(Status.FORBIDDEN, "Code.10002");
}
if (e instanceof NotBasicAuthException) {
return Result.no(Status.FORBIDDEN, "Code.10003");
}
log.warn(e.getMessage(), e);
return Result.no(Status.FORBIDDEN);
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.config; package com.yiring.auth.config;
import cn.dev33.satoken.interceptor.SaRouteInterceptor; import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.util.Auths;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
...@@ -22,24 +23,24 @@ public class SaTokenConfigure implements WebMvcConfigurer { ...@@ -22,24 +23,24 @@ public class SaTokenConfigure implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的路由拦截器 // 注册 Sa-Token 路由拦截器
// 可搭配使用注解实现鉴权: https://sa-token.dev33.cn/doc.html#/use/at-check
registry registry
.addInterceptor( .addInterceptor(
new SaRouteInterceptor((req, res, handler) -> { new SaInterceptor(handle -> {
// 登录认证 -- 拦截所有路由,并排除 /auth/** 用于开放授权相关, 以及 swagger 相关 // 登录认证 -- 拦截所有路由,并排除 /auth/** 用于开放授权相关, 以及 swagger 相关
SaRouter SaRouter
.match("/**") .match("/**")
.notMatchMethod(SaHttpMethod.OPTIONS.name()) .notMatchMethod(SaHttpMethod.OPTIONS.name())
// 实现用户权限相关后应移除下行代码
// TODO
// .notMatch("/**") // .notMatch("/**")
// 示例接口
.notMatch("/example/**") .notMatch("/example/**")
// 授权相关接口(登录、登出、注册等)
.notMatch("/auth/**") .notMatch("/auth/**")
.notMatch("/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css") .notMatch("/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css")
.notMatch("/v2/api-docs/**", "/v3/api-docs/**", "/swagger-resources/**") .notMatch("/v2/api-docs/**", "/v3/api-docs/**", "/swagger-resources/**")
.check(r -> StpUtil.checkLogin()); .check(r -> StpUtil.checkLogin());
// 管理员权限才可访问的路由地址
SaRouter.match("/sys/**", r -> StpUtil.checkRoleOr(Auths.ADMIN_ROLES.toArray(new String[0])));
}) })
) )
.addPathPatterns("/**"); .addPathPatterns("/**");
......
...@@ -6,13 +6,15 @@ import com.yiring.auth.domain.permission.Permission; ...@@ -6,13 +6,15 @@ import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 获取登录用户权限信息实现 * 获取登录用户权限信息实现
...@@ -22,11 +24,12 @@ import org.springframework.stereotype.Component; ...@@ -22,11 +24,12 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37 * 2022/3/25 9:37
*/ */
@Transactional(readOnly = true)
@Component @Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface { public class StpInterfaceImpl implements StpInterface {
@Resource final UserRepository userRepository;
UserRepository userRepository;
@Override @Override
public List<String> getPermissionList(Object loginId, String loginType) { public List<String> getPermissionList(Object loginId, String loginType) {
...@@ -36,6 +39,7 @@ public class StpInterfaceImpl implements StpInterface { ...@@ -36,6 +39,7 @@ public class StpInterfaceImpl implements StpInterface {
.stream() .stream()
.map(Role::getPermissions) .map(Role::getPermissions)
.flatMap(Set::stream) .flatMap(Set::stream)
.distinct()
.map(Permission::getUid) .map(Permission::getUid)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
...@@ -48,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface { ...@@ -48,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface {
/** /**
* 根据 id 获取用户信息 * 根据 id 获取用户信息
*
* @param loginId 登录 ID * @param loginId 登录 ID
* @return 用户信息 * @return 用户信息
*/ */
private User getUser(Object loginId) { public User getUser(Object loginId) {
String id = Objects.toString(loginId); String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id); Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) { if (optional.isEmpty()) {
throw new RuntimeException("用户不存在"); throw Status.NOT_FOUND.exception("Code.1000");
} }
return optional.get(); return optional.get();
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.permission; package com.yiring.auth.domain.permission;
import com.alibaba.fastjson.JSONObject; import static com.yiring.auth.domain.permission.Permission.DELETE_SQL;
import com.vladmihalcea.hibernate.type.json.JsonType; import static com.yiring.auth.domain.permission.Permission.TABLE_NAME;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/** /**
* 权限 * 权限
...@@ -29,19 +34,30 @@ import org.hibernate.annotations.TypeDef; ...@@ -29,19 +34,30 @@ import org.hibernate.annotations.TypeDef;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity @Entity
@TypeDef(name = "json", typeClass = JsonType.class)
@Table( @Table(
name = "SYS_PERMISSION", name = TABLE_NAME,
indexes = { @Index(columnList = "type"), @Index(columnList = "pid"), @Index(columnList = "tree") } indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Permission.Fields.type),
@Index(columnList = Permission.Fields.pid),
@Index(columnList = Permission.Fields.tree),
}
) )
@Comment("系统权限") @Comment("系统权限")
public class Permission extends BasicEntity implements Serializable { public class Permission extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_PERMISSION";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = -2001221843529000953L; private static final long serialVersionUID = -2001221843529000953L;
@Comment("类型(MENU: 菜单, BUTTON: 按钮)") @Comment("类型(MENU: 菜单, BUTTON: 按钮)")
@Column(nullable = false)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
Type type; Type type;
...@@ -49,10 +65,11 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -49,10 +65,11 @@ public class Permission extends BasicEntity implements Serializable {
Integer serial; Integer serial;
@Comment("标识") @Comment("标识")
@Column(unique = true, nullable = false) @Column(nullable = false)
String uid; String uid;
@Comment("名称") @Comment("名称")
@Column(nullable = false)
String name; String name;
@Comment("路径") @Comment("路径")
...@@ -83,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -83,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable {
* 可用于扩展一些前端可能用到的路由参数 * 可用于扩展一些前端可能用到的路由参数
*/ */
@Comment("扩展元数据信息") @Comment("扩展元数据信息")
@org.hibernate.annotations.Type(type = "json") @org.hibernate.annotations.Type(JsonType.class)
@Column(columnDefinition = "json") @Column(columnDefinition = "json")
JSONObject meta; JSONObject meta;
...@@ -107,10 +124,11 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -107,10 +124,11 @@ public class Permission extends BasicEntity implements Serializable {
/** /**
* 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整 * 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整
*
* @return JSON 格式 Meta 元数据 * @return JSON 格式 Meta 元数据
*/ */
public JSONObject getMetaJson() { public JSONObject getMetaJson() {
JSONObject meta = new JSONObject(true); JSONObject meta = new JSONObject();
meta.put("title", this.name); meta.put("title", this.name);
meta.put("icon", this.icon); meta.put("icon", this.icon);
meta.put("orderNo", this.serial); meta.put("orderNo", this.serial);
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.role; package com.yiring.auth.domain.role;
import static com.yiring.auth.domain.role.Role.DELETE_SQL;
import static com.yiring.auth.domain.role.Role.TABLE_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/** /**
* 角色 * 角色
...@@ -31,11 +37,21 @@ import org.hibernate.annotations.Comment; ...@@ -31,11 +37,21 @@ import org.hibernate.annotations.Comment;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity @Entity
@Table(
name = TABLE_NAME,
indexes = @Index(columnList = BasicEntity.Fields.deleteTime),
uniqueConstraints = { @UniqueConstraint(columnNames = { Role.Fields.uid, BasicEntity.Fields.deleteTime }) }
)
@Comment("系统角色") @Comment("系统角色")
@Table(name = "SYS_ROLE", indexes = { @Index(columnList = "uid", unique = true) })
public class Role extends BasicEntity implements Serializable { public class Role extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_ROLE";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = 910404402503275957L; private static final long serialVersionUID = 910404402503275957L;
...@@ -44,8 +60,12 @@ public class Role extends BasicEntity implements Serializable { ...@@ -44,8 +60,12 @@ public class Role extends BasicEntity implements Serializable {
String uid; String uid;
@Comment("名称") @Comment("名称")
@Column(nullable = false)
String name; String name;
@Comment("是否启用")
Boolean enable;
@JsonIgnore @JsonIgnore
@Builder.Default @Builder.Default
@Comment("权限集合") @Comment("权限集合")
......
...@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role; ...@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role;
import java.io.Serializable; import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/** /**
* 角色接口 * 角色接口
...@@ -10,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaRepository; ...@@ -10,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
* @author ifzm * @author ifzm
* 2018/9/4 8:49 * 2018/9/4 8:49
*/ */
@Repository
public interface RoleRepository extends JpaRepository<Role, Serializable> {} public interface RoleRepository extends JpaRepository<Role, Serializable> {}
...@@ -4,17 +4,19 @@ package com.yiring.auth.domain.user; ...@@ -4,17 +4,19 @@ package com.yiring.auth.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where; import org.hibernate.annotations.Where;
/** /**
...@@ -32,45 +34,49 @@ import org.hibernate.annotations.Where; ...@@ -32,45 +34,49 @@ import org.hibernate.annotations.Where;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Where(clause = "deleted = false") @SQLDelete(sql = User.DELETE_SQL)
@SQLDeleteAll(sql = User.DELETE_SQL)
@Where(clause = User.Where.EXIST)
@Entity @Entity
@Table(name = "SYS_USER", indexes = { @Index(columnList = "enabled"), @Index(columnList = "deleted") }) @Table(
name = User.TABLE_NAME,
indexes = { @Index(columnList = User.Fields.enabled), @Index(columnList = BasicEntity.Fields.deleteTime) },
uniqueConstraints = {
@UniqueConstraint(columnNames = { User.Fields.username, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.mobile, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.email, BasicEntity.Fields.deleteTime }),
}
)
@Comment("系统用户") @Comment("系统用户")
public class User extends BasicEntity implements Serializable { public class User extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_USER";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = -5787847701210907511L; private static final long serialVersionUID = -5787847701210907511L;
@Comment("头像")
String avatar;
@Comment("真实姓名") @Comment("真实姓名")
String realName; String realName;
@Comment("用户名") @Comment("用户名")
@Column(unique = true)
String username; String username;
@Comment("手机号") @Comment("手机号")
@Column(unique = true)
String mobile; String mobile;
@Comment("邮箱") @Comment("邮箱")
@Column(unique = true)
String email; String email;
@Comment("密码") @Comment("密码")
String password; String password;
@Comment("简介")
String introduction;
@Comment("头像")
String avatar;
@Comment("是否启用") @Comment("是否启用")
Boolean enabled; Boolean enabled;
@Comment("是否删除")
Boolean deleted;
@JsonIgnore @JsonIgnore
@Builder.Default @Builder.Default
@Comment("角色集合") @Comment("角色集合")
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("LoginParam") @Schema(name = "LoginParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,11 +27,11 @@ public class LoginParam implements Serializable { ...@@ -27,11 +27,11 @@ public class LoginParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456895L; private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "账号(支持用户名/手机号/邮箱)", example = "admin", required = true) @Parameter(description = "账号(支持用户名/手机号/邮箱)", example = "admin")
@NotEmpty(message = "账号不能为空") @NotEmpty(message = "账号不能为空")
String account; String account;
@ApiModelProperty(value = "密码", example = "123456", required = true) @Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空") @NotEmpty(message = "密码不能为空")
String password; String password;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("RegisterParam") @Schema(name = "RegisterParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,32 +28,29 @@ public class RegisterParam implements Serializable { ...@@ -28,32 +28,29 @@ public class RegisterParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456895L; private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "用户名", example = "admin", required = true) @Parameter(description = "用户名", example = "admin")
@NotEmpty(message = "用户名不能为空") @NotEmpty(message = "用户名不能为空")
String username; String username;
@ApiModelProperty(value = "密码", example = "123456", required = true) @Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空") @NotEmpty(message = "密码不能为空")
String password; String password;
@ApiModelProperty(value = "真实姓名", example = "管理员", required = true) @Parameter(description = "真实姓名", example = "管理员")
@NotEmpty(message = "真实姓名不能为空") @NotEmpty(message = "真实姓名不能为空")
String realName; String realName;
@ApiModelProperty(value = "手机号", example = "13012345678", required = true) @Parameter(description = "手机号", example = "13012345678")
@NotEmpty(message = "手机号不能为空") @NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确") @Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确")
String mobile; String mobile;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Parameter(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com") @Parameter(description = "邮箱", example = "developer@yiring.com")
String email; String email;
@ApiModelProperty(value = "简介", example = "平台管理员") @Parameter(description = "是否启用", example = "true")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
Boolean enable; Boolean enable;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 公共的可选父级 ID 查询参数 * 安全校验参数
* *
* @author ifzm * @author ifzm
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "OptionalPidParam", description = "公共的可选父级 ID 查询参数") @Schema(name = "SafeParam")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class OptionalPidParam implements Serializable { public class SafeParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = 9106494470582579138L;
@ApiModelProperty(value = "pid", example = "0") @Parameter(description = "密码", example = "123456")
String pid; @NotEmpty(message = "密码不能为空")
String password;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.permission; package com.yiring.auth.param.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel; import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 权限信息入参类 * 权限信息入参类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionParam") @Schema(name = "PermissionParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -31,43 +34,47 @@ public class PermissionParam implements Serializable { ...@@ -31,43 +34,47 @@ public class PermissionParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -6781934969837655538L; private static final long serialVersionUID = -6781934969837655538L;
@ApiModelProperty(value = "权限类型", example = "MENU", required = true) @Schema(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Schema(description = "权限类型", example = "MENU")
@NotNull(message = "权限类型不能为空") @NotNull(message = "权限类型不能为空")
Permission.Type type; Permission.Type type;
@ApiModelProperty(value = "序号", example = "1") @Schema(description = "序号", example = "1")
Integer serial; Integer serial;
@ApiModelProperty(value = "标识", example = "Dashboard", required = true) @Schema(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空") @NotEmpty(message = "权限标识不能为空")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "Dashboard", required = true) @Schema(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空") @NotEmpty(message = "权限名称不能为空")
String name; String name;
@ApiModelProperty(value = "路径", example = "/dashboard") @Schema(description = "路径", example = "/dashboard")
String path; String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench") @Schema(description = "重定向", example = "/dashboard/workbench")
String redirect; String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT") @Schema(description = "组件", example = "LAYOUT")
String component; String component;
@ApiModelProperty(value = "图标", example = "ion:grid-outline") @Schema(description = "图标", example = "ion:grid-outline")
String icon; String icon;
@ApiModelProperty(value = "是否隐藏", example = "false") @Schema(description = "是否隐藏", example = "false")
Boolean hidden; Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true") @Schema(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0") @Schema(description = "父级ID", example = "0")
@Builder.Default @Builder.Default
String pid = "0"; String pid = "0";
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta; JSONObject meta;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.role; package com.yiring.auth.param.role;
import io.swagger.annotations.ApiModel; import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 角色信息入参类 * 角色信息入参类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@ApiModel("RoleParam") @Schema(name = "RoleParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,11 +30,15 @@ public class RoleParam implements Serializable { ...@@ -27,11 +30,15 @@ public class RoleParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6572751635422870217L; private static final long serialVersionUID = 6572751635422870217L;
@ApiModelProperty(value = "标识", example = "admin", required = true) @Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "标识", example = "admin")
@NotEmpty(message = "角色标识不能为空") @NotEmpty(message = "角色标识不能为空")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "管理员", required = true) @Parameter(description = "名称", example = "管理员")
@NotEmpty(message = "角色名称不能为空") @NotEmpty(message = "角色名称不能为空")
String name; String name;
} }
...@@ -6,10 +6,11 @@ import cn.dev33.satoken.stp.StpUtil; ...@@ -6,10 +6,11 @@ import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Resource;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
...@@ -22,14 +23,20 @@ import org.springframework.stereotype.Component; ...@@ -22,14 +23,20 @@ import org.springframework.stereotype.Component;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Component @Component
@RequiredArgsConstructor
public class Auths { public class Auths {
@Resource final UserRepository userRepository;
UserRepository userRepository;
/**
* 管理员角色标识
*/
public static final List<String> ADMIN_ROLES = List.of("admin", "super-admin", "platform-admin", "data-admin");
/** /**
* 根据 Token 获取用户信息 * 根据 Token 获取用户信息
* 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED} * 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED}
*
* @param token token * @param token token
* @return 用户信息 * @return 用户信息
*/ */
...@@ -61,4 +68,50 @@ public class Auths { ...@@ -61,4 +68,50 @@ public class Auths {
return getUserByToken(token); return getUserByToken(token);
} }
/**
* 踢出这个用户 id 所有登录状态(可能有多人重复登录了一个账号的情况)
*
* @param userId 用户 id
*/
public void logoutAll(String userId) {
List<String> tokens = StpUtil.getTokenValueListByLoginId(userId);
for (String token : tokens) {
StpUtil.logoutByTokenValue(token);
}
}
/**
* 判断用户是否为超级管理员
*
* @param userId 用户 ID
* @return 是否为管理员
*/
public boolean isAdmin(String userId) {
Optional<User> optional = userRepository.findById(userId);
return optional.filter(this::isAdmin).isPresent();
}
/**
* 检查用户是否为管理员(检查用户是否拥有包含 admin 字符的角色)
*
* @param user 用户
* @return 是否为管理员
*/
public boolean isAdmin(User user) {
return user
.getRoles()
.stream()
.anyMatch(role -> Boolean.TRUE.equals(role.getEnable()) && ADMIN_ROLES.contains(role.getUid()));
}
/**
* 检查当前登录用户是否为管理员
* {@link this.isAdmin}
*
* @return 是否为管理员
*/
public boolean checkLoginUserIsAdmin() {
return isAdmin(getLoginUser());
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.util; package com.yiring.auth.util;
import com.sun.istack.Nullable;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.auth.vo.permission.MenuVo; import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import jakarta.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -28,6 +28,7 @@ public class Permissions { ...@@ -28,6 +28,7 @@ public class Permissions {
/** /**
* 将角色集合转换成 Vo 集合 * 将角色集合转换成 Vo 集合
*
* @param roles 角色集合 * @param roles 角色集合
* @return vos * @return vos
*/ */
...@@ -40,6 +41,7 @@ public class Permissions { ...@@ -40,6 +41,7 @@ public class Permissions {
/** /**
* 将权限集合转换成菜单树 * 将权限集合转换成菜单树
*
* @param permissions 权限集合 * @param permissions 权限集合
* @return 菜单树 * @return 菜单树
*/ */
...@@ -70,11 +72,35 @@ public class Permissions { ...@@ -70,11 +72,35 @@ public class Permissions {
} }
}); });
return roots; return new ArrayList<>(sortMenuTreeVo(roots));
}
/**
* 菜单树递归排序
*
* @param menus 菜单集合
* @return 排序后的菜单集合
*/
public static List<MenuVo> sortMenuTreeVo(@Nullable List<MenuVo> menus) {
return menus
.stream()
.sorted(
Comparator.comparing(
item -> item.getMeta().getIntValue("orderNo"),
Comparator.nullsFirst(Comparator.naturalOrder())
)
)
.peek(item -> {
if (Commons.notEmpty(item.getChildren())) {
item.setChildren(sortMenuTreeVo(item.getChildren()));
}
})
.toList();
} }
/** /**
* 将权限集合转换成 Vo 集合 * 将权限集合转换成 Vo 集合
*
* @param permissions 权限集合 * @param permissions 权限集合
* @return vos * @return vos
*/ */
...@@ -92,6 +118,7 @@ public class Permissions { ...@@ -92,6 +118,7 @@ public class Permissions {
/** /**
* 提取角色集合含有的权限去重结果 * 提取角色集合含有的权限去重结果
*
* @param roles 角色集合 * @param roles 角色集合
* @return 权限集合 * @return 权限集合
*/ */
...@@ -101,14 +128,16 @@ public class Permissions { ...@@ -101,14 +128,16 @@ public class Permissions {
.stream() .stream()
.map(Role::getPermissions) .map(Role::getPermissions)
.flatMap(Set::stream) .flatMap(Set::stream)
.distinct()
.sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length))) .sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length)))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* 根据 pid 构建树状权限集合 * 根据 pid 构建树状权限集合
*
* @param permissions 权限集合 * @param permissions 权限集合
* @param pid 权限父级 ID * @param pid 权限父级 ID
* @return 树状权限集合 * @return 树状权限集合
*/ */
public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) { public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.vo.auth; package com.yiring.auth.vo.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.*; import lombok.*;
...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("LoginVo") @Schema(name = "LoginVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,9 +26,9 @@ public class LoginVo implements Serializable { ...@@ -27,9 +26,9 @@ public class LoginVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456896L; private static final long serialVersionUID = -8690942241103456896L;
@ApiModelProperty(value = "用户 ID", example = "1") @Schema(description = "用户 ID", example = "1")
String userId; String userId;
@ApiModelProperty(value = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741") @Schema(description = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
String token; String token;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission; package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -14,13 +13,14 @@ import lombok.experimental.FieldDefaults; ...@@ -14,13 +13,14 @@ import lombok.experimental.FieldDefaults;
/** /**
* 菜单输出类 * 菜单输出类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("MenuVo") @Schema(name = "MenuVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -37,21 +37,24 @@ public class MenuVo implements Serializable { ...@@ -37,21 +37,24 @@ public class MenuVo implements Serializable {
@JsonIgnore @JsonIgnore
String pid; String pid;
@ApiModelProperty(value = "名称", example = "Dashboard") @Schema(description = "唯一标识", example = "Dashboard")
String uid;
@Schema(description = "名称", example = "Dashboard")
String name; String name;
@ApiModelProperty(value = "路径", example = "/dashboard") @Schema(description = "路径", example = "/dashboard")
String path; String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench") @Schema(description = "重定向", example = "/dashboard/workbench")
String redirect; String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT") @Schema(description = "组件", example = "LAYOUT")
String component; String component;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta; JSONObject meta;
@ApiModelProperty(value = "子权限") @Schema(description = "子权限")
List<MenuVo> children; List<MenuVo> children;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission; package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults; ...@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults;
/** /**
* 权限输出类 * 权限输出类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionVo") @Schema(name = "PermissionVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable { ...@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -9139328772148985141L; private static final long serialVersionUID = -9139328772148985141L;
@ApiModelProperty(value = "主键", example = "1") @Parameter(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "权限类型", example = "MENU") @Parameter(description = "权限类型", example = "MENU")
Permission.Type type; Permission.Type type;
@ApiModelProperty(value = "序号", example = "1") @Parameter(description = "序号", example = "1")
Integer serial; Integer serial;
@ApiModelProperty(value = "标识", example = "home") @Parameter(description = "标识", example = "home")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "主页") @Parameter(description = "名称", example = "主页")
String name; String name;
@ApiModelProperty(value = "路径", example = "/") @Parameter(description = "路径", example = "/")
String path; String path;
@ApiModelProperty(value = "组件", example = "/home") @Parameter(description = "组件", example = "/home")
String component; String component;
@ApiModelProperty(value = "图标", example = "menu") @Parameter(description = "图标", example = "menu")
String icon; String icon;
@ApiModelProperty(value = "是否隐藏", example = "false") @Parameter(description = "是否隐藏", example = "false")
Boolean hidden; Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true") @Parameter(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0") @Parameter(description = "父级ID", example = "0")
String pid; String pid;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta; JSONObject meta;
@ApiModelProperty(value = "子权限") @Parameter(description = "子权限")
List<PermissionVo> children; List<PermissionVo> children;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.role; package com.yiring.auth.vo.role;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults; ...@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults;
/** /**
* 角色响应类 * 角色响应类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@ApiModel("RoleVo") @Schema(name = "RoleVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,15 +28,15 @@ public class RoleVo implements Serializable { ...@@ -28,15 +28,15 @@ public class RoleVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -9154497137563970840L; private static final long serialVersionUID = -9154497137563970840L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "标识", example = "admin") @Schema(description = "标识", example = "admin")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "系统管理员") @Schema(description = "名称", example = "系统管理员")
String name; String name;
@ApiModelProperty("权限") @Schema(description = "权限")
List<PermissionVo> permissions; List<PermissionVo> permissions;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults; ...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserInfo") @Schema(name = "UserInfo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,25 +28,28 @@ public class UserInfoVo implements Serializable { ...@@ -28,25 +28,28 @@ public class UserInfoVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -5319037883240327088L; private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String userId; String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "手机号", example = "15616260195")
String mobile;
@Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "介绍", example = "系统管理员") @Schema(description = "介绍", example = "系统管理员")
String desc; String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty("角色") @Schema(description = "角色")
@Builder.Default @Builder.Default
List<RoleVo> roles = new ArrayList<>(0); List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench") @Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath; String homePath;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults; ...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserInfo") @Schema(name = "UserInfo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable { ...@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -5319037883240327088L; private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String userId; String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "介绍", example = "系统管理员") @Schema(description = "介绍", example = "系统管理员")
String desc; String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty("角色") @Schema(description = "角色")
@Builder.Default @Builder.Default
List<RoleVo> roles = new ArrayList<>(0); List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench") @Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath; String homePath;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
...@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults; ...@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserVo") @Schema(name = "UserVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -26,39 +26,39 @@ public class UserVo implements Serializable { ...@@ -26,39 +26,39 @@ public class UserVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -2184378273450466835L; private static final long serialVersionUID = -2184378273450466835L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "手机号", example = "13012345678") @Schema(description = "手机号", example = "13012345678")
String mobile; String mobile;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com") @Schema(description = "邮箱", example = "developer@yiring.com")
String email; String email;
@ApiModelProperty(value = "职称", example = "系统管理员") @Schema(description = "职称", example = "系统管理员")
String title; String title;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty(value = "是否启用", example = "true") @Schema(description = "是否启用", example = "true")
Boolean enabled; Boolean enabled;
@ApiModelProperty(value = "是否删除", example = "false") @Schema(description = "是否删除", example = "false")
Boolean deleted; Boolean deleted;
@ApiModelProperty(value = "最后登录IP地址", example = "127.0.0.1") @Schema(description = "最后登录IP地址", example = "127.0.0.1")
String lastLoginIp; String lastLoginIp;
@ApiModelProperty(value = "最后登录时间", example = "2022-10-24 10:24:00") @Schema(description = "最后登录时间", example = "2022-10-24 10:24:00")
LocalDateTime lastLoginTime; LocalDateTime lastLoginTime;
@ApiModelProperty(value = "最后登录时间", example = "2022-01-01 00:00:00") @Schema(description = "最后登录时间", example = "2022-01-01 00:00:00")
LocalDateTime createTime; LocalDateTime createTime;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.auth; package com.yiring.auth.web.auth;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.param.auth.LoginParam; import com.yiring.auth.param.auth.LoginParam;
import com.yiring.auth.param.auth.RegisterParam; import com.yiring.auth.param.auth.RegisterParam;
import com.yiring.auth.param.auth.SafeParam;
import com.yiring.auth.util.Auths;
import com.yiring.auth.vo.auth.LoginVo; import com.yiring.auth.vo.auth.LoginVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
...@@ -37,100 +41,124 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -37,100 +41,124 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -99) name = "Auth",
@Api(tags = "身份认证", description = "Auth") description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@RestController @RestController
@RequestMapping("/auth/") @RequestMapping("/auth/")
@RequiredArgsConstructor
public class AuthController { public class AuthController {
@Resource final Auths auths;
UserRepository userRepository; final UserRepository userRepository;
@ApiOperation(value = "注册") @Operation(summary = "注册")
@PostMapping("register") @PostMapping(value = "register")
public Result<String> register(@Valid RegisterParam param) { public Result<String> register(@ParameterObject @Validated RegisterParam param) {
// 检查用户名是否存在 // 检查用户名是否存在
long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build())); long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "用户名已存在"); throw BusinessException.i18n("Code.100000");
} }
// 检查手机号是否存在 // 检查手机号是否存在
count = userRepository.count(Example.of(User.builder().mobile(param.getMobile()).build())); count = userRepository.count(Example.of(User.builder().mobile(param.getMobile()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "手机号已存在"); throw BusinessException.i18n("Code.100001");
} }
// 检查邮箱是否存在 // 检查邮箱是否存在
if (StrUtil.isNotBlank(param.getEmail())) { if (StrUtil.isNotBlank(param.getEmail())) {
count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build())); count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "邮箱已存在"); throw BusinessException.i18n("Code.100002");
} }
} }
// 构建用户信息写入数据库 // 构建用户信息写入数据库
User user = User User user = User
.builder() .builder()
.introduction(param.getIntroduction())
.avatar(param.getAvatar()) .avatar(param.getAvatar())
.mobile(param.getMobile()) .mobile(param.getMobile())
.realName(param.getRealName()) .realName(param.getRealName())
.username(param.getUsername()) .username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword())) .password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable()) .enabled(param.getEnable())
.deleted(Boolean.FALSE)
.createTime(LocalDateTime.now())
.build(); .build();
userRepository.saveAndFlush(user); userRepository.saveAndFlush(user);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "登录") @Operation(summary = "登录")
@PostMapping("login") @PostMapping("login")
public Result<LoginVo> login(@Valid LoginParam param, HttpServletRequest request) { public Result<LoginVo> login(@ParameterObject @Validated LoginParam param, HttpServletRequest request) {
String details = "账号密码错误";
// 查询用户信息是否匹配 // 查询用户信息是否匹配
User user = userRepository.findByAccount(param.getAccount()); User user = userRepository.findByAccount(param.getAccount());
if (user == null) { if (user == null) {
return Result.no(Status.BAD_REQUEST, details); throw BusinessException.i18n("Code.100003");
} }
// 检查密码 // 检查密码
String cps = SaSecureUtil.sha256(param.getPassword()); String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) { if (!cps.equals(user.getPassword())) {
return Result.no(Status.BAD_REQUEST, details); throw BusinessException.i18n("Code.100003");
} }
// 检查用户是否已被删除 // 检查用户是否已被删除
if (!Boolean.FALSE.equals(user.getDeleted())) { if (user.getDeleteTime() != null) {
return Result.no(Status.FORBIDDEN, "用户被禁用, 请联系管理员"); throw BusinessException.i18n("Code.100004");
} }
// 检查用户是否被允许登录 // 检查用户是否被允许登录
if (!Boolean.TRUE.equals(user.getEnabled())) { if (!Boolean.TRUE.equals(user.getEnabled())) {
return Result.no(Status.FORBIDDEN, "用户被禁止登录, 请联系管理员"); throw BusinessException.i18n("Code.100005");
} }
// 登录
StpUtil.login(user.getId());
// 更新用户信息 // 更新用户信息
user.setLastLoginIp(Commons.getClientIpAddress(request)); user.setLastLoginIp(Commons.getClientIpAddress(request));
user.setLastLoginTime(LocalDateTime.now()); user.setLastLoginTime(LocalDateTime.now());
userRepository.saveAndFlush(user); userRepository.saveAndFlush(user);
// 登录
StpUtil.login(user.getId());
// 构建用户所需信息 // 构建用户所需信息
LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build(); LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build();
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "登出") @Operation(summary = "检查登录")
@GetMapping("valid")
public Result<Boolean> valid() {
return Result.ok(StpUtil.isLogin());
}
@Operation(summary = "登出")
@GetMapping("logout") @GetMapping("logout")
public Result<String> logout() { public Result<String> logout() {
StpUtil.logout(); StpUtil.logout();
return Result.ok(); return Result.ok();
} }
/**
* 二次安全校验,搭配 @SaCheckSafe 实现对关键数据不可逆操作前的二次确认
* 默认安全时间: 120s
*
* @param param 用户密码
* @link { <a href="https://sa-token.dev33.cn/doc.html#/up/safe-auth">...</a> }
*/
@SaCheckLogin
@Operation(summary = "安全验证")
@GetMapping("safe")
public Result<String> safe(@ParameterObject @Validated SafeParam param) {
User user = auths.getLoginUser();
if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) {
StpUtil.openSafe(360);
return Result.ok();
}
return Result.no();
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.permission; package com.yiring.auth.web.sys.permission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam; import com.yiring.auth.param.permission.PermissionParam;
...@@ -11,26 +9,27 @@ import com.yiring.auth.util.Permissions; ...@@ -11,26 +9,27 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam; import com.yiring.common.param.IdParam;
import com.yiring.common.param.OptionalPidParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.param.PidParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 系统权限管理控制器 * 系统权限管理控制器
...@@ -42,60 +41,56 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -42,60 +41,56 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -97) name = "权限管理",
@Api(tags = "权限管理", description = "Permission") description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@RestController @RestController
@RequestMapping("/manage/permission/") @RequestMapping("/sys/permission/")
@RequiredArgsConstructor
public class PermissionController { public class PermissionController {
@Resource final PermissionRepository permissionRepository;
PermissionRepository permissionRepository;
@ApiOperation(value = "新增") @Operation(summary = "新增")
@PostMapping("add") @PostMapping("add")
public Result<String> add(@Valid PermissionParam param) { public Result<String> add(@RequestBody @Validated({ Group.Add.class }) PermissionParam param) {
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复"); throw BusinessException.i18n("Code.1001");
} }
Permission entity = new Permission(); Permission entity = new Permission();
BeanUtils.copyProperties(param, entity); save(entity, param);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "修改") @Operation(summary = "修改")
@PostMapping("modify") @PostMapping("modify")
public Result<String> modify(@Valid PermissionParam param, @Valid IdParam idParam) { public Result<String> modify(@RequestBody @Validated({ Group.Edit.class }) PermissionParam param) {
Optional<Permission> optional = permissionRepository.findById(idParam.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission entity = optional.get(); Permission entity = optional.get();
if (!entity.getUid().equals(param.getUid())) { if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复 // 仅当修改了角色标识时才检查重复
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复"); throw BusinessException.i18n("Code.1001");
} }
} }
BeanUtils.copyProperties(param, entity); save(entity, param);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@Valid IdParam param) { public Result<String> remove(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission entity = optional.get(); Permission entity = optional.get();
...@@ -103,31 +98,33 @@ public class PermissionController { ...@@ -103,31 +98,33 @@ public class PermissionController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "查询") @Operation(summary = "查询")
@GetMapping("find") @GetMapping("find")
public Result<PermissionVo> find(@Valid IdParam param) { public Result<PermissionVo> find(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission permission = optional.get();
PermissionVo vo = new PermissionVo(); PermissionVo vo = new PermissionVo();
BeanUtils.copyProperties(optional.get(), vo); BeanUtils.copyProperties(optional.get(), vo, Permission.Fields.meta);
vo.setMeta(permission.getMetaJson());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "分页查询") @Operation(summary = "分页查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<PermissionVo>> page(@Valid PageParam param) { public Result<PageVo<PermissionVo>> page(@ParameterObject @Validated PageParam param) {
Page<Permission> page = permissionRepository.findAll(PageParam.toPageable(param)); Page<Permission> page = permissionRepository.findAll(PageParam.toPageable(param));
List<PermissionVo> data = Permissions.toPermissionVos(page.toList()); List<PermissionVo> data = Permissions.toPermissionVos(page.toList());
PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements()); PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "树结构查询") @Operation(summary = "树结构查询")
@GetMapping(value = "tree") @GetMapping(value = "tree")
public Result<ArrayList<PermissionVo>> tree(OptionalPidParam param) { public Result<ArrayList<PermissionVo>> tree(@ParameterObject @Validated(Group.Optional.class) PidParam param) {
List<Permission> permissions = permissionRepository.findAll(); List<Permission> permissions = permissionRepository.findAll();
List<PermissionVo> vos = Permissions.toTree( List<PermissionVo> vos = Permissions.toTree(
permissions, permissions,
...@@ -138,6 +135,7 @@ public class PermissionController { ...@@ -138,6 +135,7 @@ public class PermissionController {
/** /**
* 根据父级 ID 获取当前树节点标识 * 根据父级 ID 获取当前树节点标识
*
* @param pid 父级 ID * @param pid 父级 ID
* @return 树节点标识 * @return 树节点标识
*/ */
...@@ -152,6 +150,7 @@ public class PermissionController { ...@@ -152,6 +150,7 @@ public class PermissionController {
/** /**
* 检查是否存在已有相同标识的权限 * 检查是否存在已有相同标识的权限
*
* @param uid 权限标识 * @param uid 权限标识
* @return 是否存在 * @return 是否存在
*/ */
...@@ -159,4 +158,16 @@ public class PermissionController { ...@@ -159,4 +158,16 @@ public class PermissionController {
Permission entity = Permission.builder().uid(uid).build(); Permission entity = Permission.builder().uid(uid).build();
return permissionRepository.count(Example.of(entity)) > 0; return permissionRepository.count(Example.of(entity)) > 0;
} }
/**
* 新增或修改权限菜单
*
* @param entity 实体对象
* @param param 参数
*/
private void save(Permission entity, PermissionParam param) {
BeanUtils.copyProperties(param, entity, Permission.Fields.meta);
entity.setTree(getTreeNode(param.getPid()));
permissionRepository.saveAndFlush(entity);
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.role; package com.yiring.auth.web.sys.role;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
...@@ -11,17 +10,21 @@ import com.yiring.auth.util.Permissions; ...@@ -11,17 +10,21 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam; import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam; import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import lombok.RequiredArgsConstructor;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
...@@ -41,24 +44,24 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -41,24 +44,24 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -96) name = "角色管理",
@Api(tags = "角色管理", description = "Role") description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@RestController @RestController
@RequestMapping("/manage/role/") @RequestMapping("/sys/role/")
@RequiredArgsConstructor
public class RoleController { public class RoleController {
@Resource final RoleRepository roleRepository;
RoleRepository roleRepository; final PermissionRepository permissionRepository;
@Resource @Operation(summary = "新增")
PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@PostMapping("add") @PostMapping("add")
public Result<String> add(@Valid RoleParam param) { public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) RoleParam param) {
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复"); throw BusinessException.i18n("Code.1002");
} }
Role entity = new Role(); Role entity = new Role();
...@@ -67,19 +70,19 @@ public class RoleController { ...@@ -67,19 +70,19 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "修改") @Operation(summary = "修改")
@PostMapping("modify") @PostMapping("modify")
public Result<String> modify(@Valid RoleParam param, @Valid IdParam idParam) { public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) RoleParam param) {
Optional<Role> optional = roleRepository.findById(idParam.getId()); Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Role entity = optional.get(); Role entity = optional.get();
if (!entity.getUid().equals(param.getUid())) { if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复 // 仅当修改了角色标识时才检查重复
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复"); throw BusinessException.i18n("Code.1002");
} }
} }
...@@ -88,21 +91,20 @@ public class RoleController { ...@@ -88,21 +91,20 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "分配权限") @Operation(summary = "分配权限")
@PostMapping("assign") @PostMapping("assign")
public Result<String> assign(@Valid IdParam idParam, @Valid IdsParam idsParam) { public Result<String> assign(
@ParameterObject @Validated IdParam idParam,
@ParameterObject @Validated IdsParam idsParam
) {
Optional<Role> optional = roleRepository.findById(idParam.getId()); Optional<Role> optional = roleRepository.findById(idParam.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
// 查询权限集合 // 查询权限集合
Set<String> ids = idsParam.toIds(); Collection<Serializable> ids = idsParam.toIds();
Set<Permission> permissions = permissionRepository Set<Permission> permissions = new HashSet<>(permissionRepository.findAllById(ids));
.findAll()
.stream()
.filter(permission -> ids.contains(permission.getId()))
.collect(Collectors.toSet());
Role entity = optional.get(); Role entity = optional.get();
entity.setPermissions(permissions); entity.setPermissions(permissions);
...@@ -110,25 +112,20 @@ public class RoleController { ...@@ -110,25 +112,20 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@Valid IdParam param) { public Result<String> remove(@ParameterObject @Validated IdsParam param) {
Optional<Role> optional = roleRepository.findById(param.getId()); List<Role> roles = roleRepository.findAllById(param.toIds());
if (optional.isEmpty()) { roleRepository.deleteAll(roles);
return Result.no(Status.NOT_FOUND);
}
Role entity = optional.get();
roleRepository.delete(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "查询") @Operation(summary = "查询")
@GetMapping("find") @GetMapping("find")
public Result<RoleVo> find(@Valid IdParam param) { public Result<RoleVo> find(@ParameterObject @Validated IdParam param) {
Optional<Role> optional = roleRepository.findById(param.getId()); Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Role entity = optional.get(); Role entity = optional.get();
...@@ -138,16 +135,16 @@ public class RoleController { ...@@ -138,16 +135,16 @@ public class RoleController {
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "分页查询") @Operation(summary = "分页查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<RoleVo>> page(@Valid PageParam param) { public Result<PageVo<RoleVo>> page(@ParameterObject @Validated PageParam param) {
Page<Role> page = roleRepository.findAll(PageParam.toPageable(param)); Page<Role> page = roleRepository.findAll(PageParam.toPageable(param));
List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet())); List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet()));
PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements()); PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "选项查询") @Operation(summary = "选项查询")
@GetMapping("selector") @GetMapping("selector")
public Result<ArrayList<RoleVo>> selector() { public Result<ArrayList<RoleVo>> selector() {
List<Role> roles = roleRepository.findAll(); List<Role> roles = roleRepository.findAll();
...@@ -157,6 +154,7 @@ public class RoleController { ...@@ -157,6 +154,7 @@ public class RoleController {
/** /**
* 检查是否存在已有相同标识的角色 * 检查是否存在已有相同标识的角色
*
* @param uid 角色标识 * @param uid 角色标识
* @return 是否存在 * @return 是否存在
*/ */
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.user;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.vo.user.UserVo;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.util.Commons;
import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统用户控制器
*
* @author Jim
* @version 0.1
* 2022/1/24 14:13
*/
@Slf4j
@Validated
@Tag(
name = "用户管理",
description = "User",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-97") }) }
)
@RestController
@RequestMapping("/sys/user/")
@RequiredArgsConstructor
public class UserController {
final UserRepository userRepository;
final RoleRepository roleRepository;
@Operation(summary = "分配角色")
@PostMapping("assign")
public Result<String> assign(
@ParameterObject @Validated IdParam idParam,
@ParameterObject @Validated IdsParam idsParam
) {
Optional<User> optional = userRepository.findById(idParam.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
// 查询角色集合
Collection<Serializable> ids = idsParam.toIds();
Set<Role> roles = new HashSet<>(roleRepository.findAllById(ids));
User entity = optional.get();
entity.setRoles(roles);
userRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<UserVo>> page(@ParameterObject @Validated PageParam param) {
Page<User> page = userRepository.findAll(PageParam.toPageable(param));
List<UserVo> data = page.get().map(user -> Commons.transform(user, UserVo.class)).collect(Collectors.toList());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.user; package com.yiring.auth.web.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.util.Auths; import com.yiring.auth.util.Auths;
import com.yiring.auth.util.Permissions; import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.MenuVo; import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo; import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.auth.vo.user.UserVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import io.swagger.v3.oas.annotations.Operation;
import com.yiring.common.param.IdParam; import io.swagger.v3.oas.annotations.extensions.Extension;
import com.yiring.common.param.IdsParam; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import com.yiring.common.param.PageParam; import io.swagger.v3.oas.annotations.tags.Tag;
import com.yiring.common.util.Commons;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
...@@ -46,39 +32,35 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -46,39 +32,35 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -95) name = "用户信息",
@Api(tags = "用户管理", description = "User") description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@RestController @RestController
@RequestMapping("/user/") @RequestMapping("/user/")
public class UserController { @RequiredArgsConstructor
public class UserViewController {
@Resource final Auths auths;
Auths auths;
@Resource @Operation(summary = "获取登录用户信息")
UserRepository userRepository;
@Resource
RoleRepository roleRepository;
@ApiOperation(value = "获取登录用户信息")
@GetMapping("getUserInfo") @GetMapping("getUserInfo")
public Result<UserInfoVo> getUserInfo() { public Result<UserInfoVo> getUserInfo() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
UserInfoVo userInfoVo = UserInfoVo UserInfoVo userInfoVo = UserInfoVo
.builder() .builder()
.userId(user.getId()) .userId(user.getId())
.mobile(user.getMobile())
.username(user.getUsername()) .username(user.getUsername())
.realName(user.getRealName()) .realName(user.getRealName())
.avatar(user.getAvatar()) .avatar(user.getAvatar())
.desc(user.getIntroduction())
.roles(Permissions.toRoleVos(user.getRoles())) .roles(Permissions.toRoleVos(user.getRoles()))
.build(); .build();
return Result.ok(userInfoVo); return Result.ok(userInfoVo);
} }
@ApiOperation(value = "获取用户菜单") @Operation(summary = "获取用户菜单")
@GetMapping("getMenuList") @GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() { public Result<ArrayList<MenuVo>> getMenuList() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
...@@ -91,7 +73,7 @@ public class UserController { ...@@ -91,7 +73,7 @@ public class UserController {
return Result.ok((ArrayList<MenuVo>) vos); return Result.ok((ArrayList<MenuVo>) vos);
} }
@ApiOperation(value = "获取用户权限") @Operation(summary = "获取用户权限")
@GetMapping("getPermCode") @GetMapping("getPermCode")
public Result<ArrayList<String>> getPermCode() { public Result<ArrayList<String>> getPermCode() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
...@@ -99,36 +81,4 @@ public class UserController { ...@@ -99,36 +81,4 @@ public class UserController {
List<String> codes = permissions.stream().map(Permission::getUid).collect(Collectors.toList()); List<String> codes = permissions.stream().map(Permission::getUid).collect(Collectors.toList());
return Result.ok((ArrayList<String>) codes); return Result.ok((ArrayList<String>) codes);
} }
@ApiOperation(value = "分配角色")
@PostMapping("/manage/assign")
public Result<String> assign(@Valid IdParam idParam, @Valid IdsParam idsParam) {
Optional<User> optional = userRepository.findById(idParam.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
}
// 查询权限集合
Set<String> ids = idsParam.toIds();
Set<Role> roles = roleRepository
.findAll()
.stream()
.filter(role -> ids.contains(role.getId()))
.collect(Collectors.toSet());
User entity = optional.get();
entity.setRoles(roles);
userRepository.saveAndFlush(entity);
return Result.ok();
}
@ApiOperation(value = "分页查询")
@GetMapping("/manage/page")
public Result<PageVo<UserVo>> page(@Valid PageParam param) {
Page<User> page = userRepository.findAll(PageParam.toPageable(param));
List<UserVo> data = page.get().map(user -> Commons.transform(user, UserVo.class)).collect(Collectors.toList());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
} }
# Sa-Token配置 # Sa-Token配置
sa-token: sa-token:
# token名称 (同时也是cookie名称) # token名称 (同时也是cookie名称)
token-name: Authorization token-name: App-Token
# token有效期,单位s 默认30天, -1代表永不过期 # token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000 timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
......
Code.1000=\u7528\u6237\u4E0D\u5B58\u5728
Code.1001=\u6743\u9650\u6807\u8BC6\u91CD\u590D
Code.1002=\u89D2\u8272\u6807\u8BC6\u91CD\u590D
Code.10000=\u4E8C\u7EA7\u8BA4\u8BC1\u6821\u9A8C\u5931\u8D25
Code.10001=\u89D2\u8272\u6743\u9650\u4E0D\u8DB3
Code.10002=\u6743\u9650\u4E0D\u8DB3
Code.10003=\u672A\u6388\u6743
Code.100000=\u7528\u6237\u540D\u5DF2\u5B58\u5728
Code.100001=\u624B\u673A\u53F7\u5DF2\u5B58\u5728
Code.100002=\u90AE\u7BB1\u5DF2\u5B58\u5728
Code.100003=\u8D26\u53F7\u5BC6\u7801\u9519\u8BEF
Code.100004=\u7528\u6237\u88AB\u7981\u7528, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.100005=\u7528\u6237\u88AB\u7981\u6B62\u767B\u5F55, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.1000=\u7528\u6237\u4E0D\u5B58\u5728
Code.1001=\u6743\u9650\u6807\u8BC6\u91CD\u590D
Code.1002=\u89D2\u8272\u6807\u8BC6\u91CD\u590D
Code.10000=\u4E8C\u7EA7\u8BA4\u8BC1\u6821\u9A8C\u5931\u8D25
Code.10001=\u89D2\u8272\u6743\u9650\u4E0D\u8DB3
Code.10002=\u6743\u9650\u4E0D\u8DB3
Code.10003=\u672A\u6388\u6743
Code.100000=\u7528\u6237\u540D\u5DF2\u5B58\u5728
Code.100001=\u624B\u673A\u53F7\u5DF2\u5B58\u5728
Code.100002=\u90AE\u7BB1\u5DF2\u5B58\u5728
Code.100003=\u8D26\u53F7\u5BC6\u7801\u9519\u8BEF
Code.100004=\u7528\u6237\u88AB\u7981\u7528, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.100005=\u7528\u6237\u88AB\u7981\u6B62\u767B\u5F55, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Status.OK=OK
Status.NON_AUTHORITATIVE_INFORMATION=Non-Authoritative Information
Status.BAD_REQUEST=Bad Request
Status.UNAUTHORIZED=Unauthorized
Status.FORBIDDEN=Forbidden
Status.NOT_FOUND=Not Found
Status.METHOD_NOT_ALLOWED=Method Not Allowed
Status.EXPECTATION_FAILED=Expectation Failed
Status.INTERNAL_SERVER_ERROR=Internal Server Error
Status.UNKNOWN_ERROR=Unknown Error
Status.NOT_IMPLEMENTED=Not Implemented
Status.BAD_GATEWAY=Bad Gateway
Status.SERVICE_UNAVAILABLE=Service Unavailable
Status.OK=\u6210\u529F
Status.NON_AUTHORITATIVE_INFORMATION=\u8BA4\u8BC1\u5931\u8D25
Status.BAD_REQUEST=\u8BF7\u6C42\u5931\u8D25
Status.UNAUTHORIZED=\u51ED\u8BC1\u8FC7\u671F
Status.FORBIDDEN=\u7981\u6B62\u8BBF\u95EE
Status.NOT_FOUND=\u627E\u4E0D\u5230\u8D44\u6E90
Status.METHOD_NOT_ALLOWED=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u7C7B\u578B
Status.EXPECTATION_FAILED=\u65E0\u6548\u53C2\u6570
Status.INTERNAL_SERVER_ERROR=\u670D\u52A1\u5668\u9519\u8BEF
Status.UNKNOWN_ERROR=\u672A\u77E5\u9519\u8BEF
Status.NOT_IMPLEMENTED=API \u672A\u5B9E\u73B0
Status.BAD_GATEWAY=\u670D\u52A1\u5F02\u5E38
Status.SERVICE_UNAVAILABLE=\u670D\u52A1\u6682\u505C
dependencies { dependencies {
implementation project(":basic-common:util") implementation project(":basic-common:util")
implementation project(":basic-common:i18n")
implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 本地依赖 // 本地依赖
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar']) implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-extra // hutool-extra
implementation "cn.hutool:hutool-extra:${hutoolVersion}" implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// JTS 几何对象操作库 // JTS 几何对象操作库
implementation "org.locationtech.jts:jts-core:${jtsVersion}" implementation "org.locationtech.jts:jts-core:${jtsVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
// hypersistence-utils-hibernate-60
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.n52.jackson/jackson-datatype-jts/1.2.10 // https://mvnrepository.com/artifact/org.n52.jackson/jackson-datatype-jts/1.2.10
implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") { implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") {
exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.locationtech.jts' exclude group: 'org.locationtech.jts'
} }
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
} }
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.annotation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.lang.annotation.*;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.MediaType;
/**
* 下载响应注解
*
* @author Jim
* @version 0.1
* 2023/1/12 11:22
*/
@SuppressWarnings({ "unused" })
@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@ApiResponse
public @interface DownloadResponse {
@AliasFor(annotation = ApiResponse.class)
String responseCode() default "200";
@AliasFor(annotation = ApiResponse.class)
String description() default "OK";
@AliasFor(annotation = ApiResponse.class)
Content content() default @Content(
schema = @Schema(type = "file", format = "binary"),
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE
);
}
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.aspect; package com.yiring.common.aspect;
import cn.hutool.extra.servlet.ServletUtil; import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter; import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.servlet.http.HttpServletRequest; import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
...@@ -34,6 +35,11 @@ public class RequestAspect { ...@@ -34,6 +35,11 @@ public class RequestAspect {
@Value("${debug}") @Value("${debug}")
Boolean debug; Boolean debug;
/**
* 白名单(忽略)
*/
List<String> IGNORE_LIST = List.of("/swagger-resources", "/error", "/v3/api-docs");
@Pointcut( @Pointcut(
"@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)" "@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)"
) )
...@@ -42,20 +48,33 @@ public class RequestAspect { ...@@ -42,20 +48,33 @@ public class RequestAspect {
@Around("apiPointCut()") @Around("apiPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest(); HttpServletRequest request = getRequest();
// 放行白名单
for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) {
return point.proceed();
}
}
// 计算接口执行耗时 // 计算接口执行耗时
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Object result = point.proceed(); Object result = point.proceed();
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
// 计算接口处理消耗时间,格式化
String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME); String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME);
String times = String.format("%.3fs", (double) (end - start) / 1000); String times = String.format("%.3fs", (double) (end - start) / 1000);
// Print Request Log (Optional Replace: MDC) // 获取接口请求扩展信息,Header, Params
String extra = ""; String extra = "";
if (Boolean.TRUE.equals(debug)) { if (Boolean.TRUE.equals(debug)) {
String headers = JSONObject.toJSONString(ServletUtil.getHeaderMap(request), SerializerFeature.PrettyFormat); String headers = JSONObject.toJSONString(
String params = JSONObject.toJSONString(ServletUtil.getParamMap(request), SerializerFeature.PrettyFormat); JakartaServletUtil.getHeaderMap(request),
JSONWriter.Feature.PrettyFormat
);
String params = JSONObject.toJSONString(
JakartaServletUtil.getParamMap(request),
JSONWriter.Feature.PrettyFormat
);
extra += String.format("\nHeaders: %s", headers); extra += String.format("\nHeaders: %s", headers);
extra += String.format("\nParams: %s", params); extra += String.format("\nParams: %s", params);
if (result instanceof Result) { if (result instanceof Result) {
...@@ -67,20 +86,26 @@ public class RequestAspect { ...@@ -67,20 +86,26 @@ public class RequestAspect {
); );
} }
} }
// 获取接口处理返回的状态码,设置接口响应时间和耗时信息
int status = 200;
if (result instanceof Result) {
((Result<?>) result).setTimestamp(timestamp);
((Result<?>) result).setTimes(times);
status = ((Result<?>) result).getStatus();
}
// 打印请求日志 (Optional Replace: MDC, Trace)
log.info( log.info(
"[Request] Method: {}, URL: {}, IP: {}, Times: {}{}", "[Request] Method: {}, URL: {}, Status: {}, IP: {}, Times: {}{}",
request.getMethod(), request.getMethod(),
request.getRequestURL(), request.getRequestURL(),
status,
Commons.getClientIpAddress(request), Commons.getClientIpAddress(request),
times, times,
extra extra
); );
if (result instanceof Result) {
((Result<?>) result).setTimestamp(timestamp);
((Result<?>) result).setTimes(times);
}
return result; return result;
} }
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.config;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.exception.FailStatusException;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.aspectj.bridge.AbortException;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 核心异常拦截处理
*
* @author Jim
* @version 0.1
* 2023/1/12 14:06
*/
@Slf4j
@Order(0)
@RestControllerAdvice
@RequiredArgsConstructor
public class CoreExceptionHandler {
final I18n i18n;
/**
* 参数校验异常
*
* @param e 异常信息
* @return 统一的校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(
{ BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
)
public Result<String> validFailHandler(Exception e) {
String details = null;
if (e instanceof ConstraintViolationException) {
details = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
} else {
BindingResult result = null;
if (e instanceof MethodArgumentNotValidException) {
result = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
result = ((BindException) e).getBindingResult();
}
if (result != null) {
ObjectError error = result.getAllErrors().iterator().next();
if (error instanceof FieldError fieldError) {
// 构建明确的字段错误提示, 例如: id 不能为 null, 如果自己填写了 message 则不追加 field 字段前缀
ConstraintViolationImpl<?> violation = error.unwrap(ConstraintViolationImpl.class);
String template = violation.getMessageTemplate();
String prefix = "";
// 如果是模板字符串, 则在消息前添加字段提示
if (template.contains("{") && template.contains("}")) {
prefix = "参数" + fieldError.getField();
}
details = prefix + i18n.get(fieldError);
} else {
details = i18n.get(error);
}
}
}
return Result.no(Status.EXPECTATION_FAILED, details);
}
/**
* 参数读取解析失败异常
* eg: 例如参数使用 RequestBody 接收,但是传了个空字符串,导致解析失败
*
* @param e 异常
* @return 校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<String> httpMessageNotReadableExceptionHandler(Exception e) {
log.warn(e.getMessage(), e);
return Result.no(Status.EXPECTATION_FAILED);
}
/**
* 不支持的HttpMethod异常
*
* @param e 异常信息
* @return 异常信息反馈 {@link Status#METHOD_NOT_ALLOWED
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<String> httpRequestMethodNotSupportedErrorHandler(Exception e) {
return Result.no(Status.METHOD_NOT_ALLOWED, e.getMessage());
}
/**
* 自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<String> businessExceptionHandler(BusinessException e) {
return Result.no(e.getStatus(), e.getMessage());
}
/**
* 失败状态异常
*/
@ExceptionHandler(FailStatusException.class)
public Result<String> failStatusExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus(), e.getMessage());
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler({ ClientAbortException.class, AbortException.class, HttpMessageNotWritableException.class })
public void ignoreExceptionHandler() {}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.config; package com.yiring.common.config;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,12 +19,71 @@ import org.springframework.context.annotation.Configuration; ...@@ -17,12 +19,71 @@ import org.springframework.context.annotation.Configuration;
@Data @Data
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration @Configuration("env.config")
@ConfigurationProperties(prefix = "env") @ConfigurationProperties(prefix = "env")
public class EnvConfig { public class EnvConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1017213697767634790L;
/** /**
* host,用来共享一些资源(如:数据库、文件存储等相关的依赖源) * host,用来共享一些资源(如:数据库、文件存储等相关的依赖源)
*/ */
String host; String host;
/**
* 是否为生产环境
*/
boolean prod;
/**
* FFmpeg 配置
*/
FFmpeg ffmpeg;
/**
* 扩展配置
*/
Extra extra;
/**
* FFmpeg 路径相关配置
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.ffmpeg")
@ConfigurationProperties(prefix = "env.ffmpeg")
public static class FFmpeg implements Serializable {
@Serial
private static final long serialVersionUID = 972365906869473179L;
/**
* ffmpeg 路径
*/
String path;
}
/**
* 扩展环境配置变量
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.extra")
@ConfigurationProperties(prefix = "env.extra")
public static class Extra implements Serializable {
@Serial
private static final long serialVersionUID = -521508901960998020L;
/**
* 公共用户名
*/
String username;
/**
* 公共密码
*/
String password;
}
} }
...@@ -4,7 +4,7 @@ package com.yiring.common.config; ...@@ -4,7 +4,7 @@ package com.yiring.common.config;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import org.n52.jackson.datatype.jts.JtsModule; import org.n52.jackson.datatype.jts.JtsModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration; ...@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
@RequiredArgsConstructor
public class JacksonConfig { public class JacksonConfig {
@Resource final JavaTimeModule javaTimeModule;
JavaTimeModule javaTimeModule;
@Bean @Bean
public ObjectMapper objectMapper() { public ObjectMapper objectMapper() {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.core; package com.yiring.common.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.PropertyKey;
/** /**
* 标准的响应对象(所有的接口响应内容格式都应该是一致的) * 标准的响应对象(所有的接口响应内容格式都应该是一致的)
...@@ -21,63 +23,70 @@ import lombok.extern.slf4j.Slf4j; ...@@ -21,63 +23,70 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@SuppressWarnings({ "unchecked", "unused" }) @SuppressWarnings({ "unchecked", "unused" })
@ApiModel("Result")
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@Slf4j
@Data @Data
@Builder @Builder
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Result<T extends Serializable> implements Serializable { public class Result<T> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -4802543396830024571L; private static final long serialVersionUID = -4802543396830024571L;
/** /**
* 注入 I18n
*/
protected static final I18n i18n;
static {
i18n = SpringUtil.getBean(I18n.class);
}
/**
* 接口响应时间 * 接口响应时间
*/ */
@ApiModelProperty(value = "响应时间", example = "2021-01-01 00:00:00") @Schema(description = "响应时间", example = "2021-01-01 00:00:00")
String timestamp; String timestamp;
/** /**
* 接口耗时(单位:秒)通常在调试阶段出现 * 接口耗时(单位:秒)通常在调试阶段出现
*/ */
@ApiModelProperty(value = "耗时", example = "0.001s") @Schema(description = "耗时", example = "0.001s")
String times; String times;
/** /**
* 响应状态码 * 响应状态码
*/ */
@ApiModelProperty(value = "状态码", example = "200") @Schema(description = "状态码", example = "200", defaultValue = "200")
Integer status; Integer status;
/** /**
* 业务标识码 * 响应消息
*/ */
@ApiModelProperty(value = "业务标识码", example = "0") @Schema(description = "消息", example = "OK", defaultValue = "OK")
Integer code; String message;
/** /**
* 响应消息 * 业务标识码
*/ */
@ApiModelProperty(value = "消息", example = "OK") @Schema(description = "业务标识码", example = "0", nullable = true)
String message; Integer code;
/** /**
* 详细信息,通常为参数校验结果或自定义消息 * 详细信息,通常为参数校验结果或自定义消息
*/ */
@ApiModelProperty(value = "详细信息", example = "Details message") @Schema(description = "详细信息", nullable = true)
String details; String details;
/** /**
* 异常信息,通常在出现服务器错误时会出现该异常 * 异常信息,通常在出现服务器错误时会出现该异常
*/ */
@ApiModelProperty(value = "异常信息", notes = "出现错误时会出现该字段", example = "Error message") @Schema(description = "异常信息", nullable = true)
String error; String error;
/** /**
* 响应内容 * 响应内容
*/ */
@ApiModelProperty("内容") @Schema(description = "内容")
T body; T body;
/** /**
...@@ -86,8 +95,24 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -86,8 +95,24 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see com.yiring.common.core.Status * @see com.yiring.common.core.Status
*/ */
public static <T extends Serializable> Result<T> ok() { public static <T> Result<T> ok() {
return (Result<T>) Result.builder().status(Status.OK.value()).message(Status.OK.getReasonPhrase()).build(); return (Result<T>) Result.builder().status(Status.OK.value()).message(t(Status.OK.getReasonPhrase())).build();
}
/**
* 返回成功响应内容
*
* @param body {@link String} {@link I18n}
* @return Result
* @see com.yiring.common.core.Status
*/
public static <T> Result<T> ok(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String body) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
.message(t(Status.OK.getReasonPhrase()))
.body(t(body))
.build();
} }
/** /**
...@@ -96,22 +121,42 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -96,22 +121,42 @@ public class Result<T extends Serializable> implements Serializable {
* @param body {@link Object} * @param body {@link Object}
* @return Result * @return Result
*/ */
public static <T extends Serializable> Result<T> ok(T body) { public static <T> Result<T> ok(T body) {
return (Result<T>) Result return (Result<T>) Result
.builder() .builder()
.status(Status.OK.value()) .status(Status.OK.value())
.message(Status.OK.getReasonPhrase()) .message(t(Status.OK.getReasonPhrase()))
.body(body) .body(body)
.build(); .build();
} }
/** /**
* 返回默认的 400 错误响应
*
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T> Result<T> no() {
return no(Status.BAD_REQUEST);
}
/**
* 返回默认的 400 错误响应
*
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T> Result<T> no(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(Status.BAD_REQUEST, details);
}
/**
* 返回失败响应内容 * 返回失败响应内容
* *
* @return Result * @return Result
* @see Status#BAD_REQUEST * @see Status#BAD_REQUEST
*/ */
public static <T extends Serializable> Result<T> no(Status status) { public static <T> Result<T> no(Status status) {
return no(status, null, null, null); return no(status, null, null, null);
} }
...@@ -121,7 +166,7 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -121,7 +166,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, String details) { public static <T> Result<T> no(Status status, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(status, null, details, null); return no(status, null, details, null);
} }
...@@ -131,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -131,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, Throwable error) { public static <T> Result<T> no(Status status, Throwable error) {
return no(status, null, null, error); return no(status, null, null, error);
} }
...@@ -141,13 +186,28 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -141,13 +186,28 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, Integer code, String details, Throwable error) { public static <T> Result<T> no(
Status status,
Integer code,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error
) {
if (Objects.isNull(code) && Objects.nonNull(details)) {
String prefix = "Code.";
if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, "");
code = Convert.toInt(codeText);
} else {
code = -1;
}
}
Result<T> result = (Result<T>) Result Result<T> result = (Result<T>) Result
.builder() .builder()
.status(status.value()) .status(status.value())
.message(status.getReasonPhrase()) .message(t(status.getReasonPhrase()))
.code(code) .code(code)
.details(details) .details(t(details))
.build(); .build();
if (error != null) { if (error != null) {
...@@ -156,4 +216,18 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -156,4 +216,18 @@ public class Result<T extends Serializable> implements Serializable {
return result; return result;
} }
/**
* i18n 默认值获取简单包装
*
* @param message 文本消息
* @return i18n 翻译结果文本消息
*/
public static String t(String message) {
if (message == null) {
return null;
}
return i18n.get(message, message);
}
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.core; package com.yiring.common.core;
import com.yiring.common.exception.FailStatusException;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* API 响应状态码 * API 响应状态码
* 包含系统和业务两个维度 * 包含系统和业务两个维度
*
* @author ifzm
*/ */
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
...@@ -13,73 +17,73 @@ public enum Status { ...@@ -13,73 +17,73 @@ public enum Status {
/** /**
* 成功 * 成功
*/ */
OK(200, "OK"), OK(200, "Status.OK"),
/** /**
* 用户认证失败 * 用户认证失败
*/ */
NON_AUTHORITATIVE_INFORMATION(203, "认证失败"), NON_AUTHORITATIVE_INFORMATION(203, "Status.NON_AUTHORITATIVE_INFORMATION"),
/** /**
* 失败的请求,通常是一些验证错误 * 失败的请求,通常是一些验证错误
*/ */
BAD_REQUEST(400, "FAIL"), BAD_REQUEST(400, "Status.BAD_REQUEST"),
/** /**
* 鉴权失败 * 鉴权失败
*/ */
UNAUTHORIZED(401, "凭证过期"), UNAUTHORIZED(401, "Status.UNAUTHORIZED"),
/** /**
* Token 错误/失效 * 禁止访问(可能是二级认证失败)
*/ */
FORBIDDEN(403, "禁止访问"), FORBIDDEN(403, "Status.FORBIDDEN"),
/** /**
* 找不到资源 * 找不到资源
*/ */
NOT_FOUND(404, "Not Found"), NOT_FOUND(404, "Status.NOT_FOUND"),
/** /**
* 不支持的请求类型 * 不支持的请求类型
*/ */
METHOD_NOT_ALLOWED(405, "不支持的请求类型"), METHOD_NOT_ALLOWED(405, "Status.METHOD_NOT_ALLOWED"),
/** /**
* 参数校验失败 * 参数校验失败
*/ */
EXPECTATION_FAILED(417, "无效参数"), EXPECTATION_FAILED(417, "Status.EXPECTATION_FAILED"),
/** /**
* 服务器错误 * 服务器错误
*/ */
INTERNAL_SERVER_ERROR(500, "服务器错误"), INTERNAL_SERVER_ERROR(500, "Status.INTERNAL_SERVER_ERROR"),
/** /**
* 未知错误 * 未知错误
*/ */
UNKNOWN_ERROR(500, "未知错误"), UNKNOWN_ERROR(500, "Status.UNKNOWN_ERROR"),
/** /**
* API 未实现 * API 未实现
*/ */
NOT_IMPLEMENTED(501, "API 未实现"), NOT_IMPLEMENTED(501, "Status.NOT_IMPLEMENTED"),
/** /**
* 服务异常(网关提醒) * 服务异常(网关提醒)
*/ */
BAD_GATEWAY(502, "Bad Gateway"), BAD_GATEWAY(502, "Status.BAD_GATEWAY"),
/** /**
* 服务暂停(网关提醒) * 服务暂停(网关提醒)
*/ */
SERVICE_UNAVAILABLE(503, "Service Unavailable"); SERVICE_UNAVAILABLE(503, "Status.SERVICE_UNAVAILABLE");
private final int value; private final int value;
private final String reasonPhrase; private final String reasonPhrase;
Status(int value, String reasonPhrase) { Status(int value, @PropertyKey(resourceBundle = "i18n.status") String reasonPhrase) {
this.value = value; this.value = value;
this.reasonPhrase = reasonPhrase; this.reasonPhrase = reasonPhrase;
} }
...@@ -137,4 +141,29 @@ public enum Status { ...@@ -137,4 +141,29 @@ public enum Status {
public String toString() { public String toString() {
return this.value + " " + name(); return this.value + " " + name();
} }
/**
* 快速失败异常
*/
public FailStatusException exception() {
return exception(null);
}
/**
* 快速失败异常
*
* @param message 异常消息
*/
public FailStatusException exception(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
return new FailStatusException(this, message);
}
/**
* 暴露异常
*
* @param message 异常消息
*/
public void expose(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) throws FailStatusException {
throw exception(message);
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.domain; package com.yiring.common.domain;
import com.yiring.common.snowflake.SnowflakeId;
import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
...@@ -14,7 +12,7 @@ import org.hibernate.annotations.Comment; ...@@ -14,7 +12,7 @@ import org.hibernate.annotations.Comment;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.snowflake.SnowflakeId; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/** /**
* 基础表抽象类 * 基础表抽象类
...@@ -33,6 +31,7 @@ import org.hibernate.snowflake.SnowflakeId; ...@@ -33,6 +31,7 @@ import org.hibernate.snowflake.SnowflakeId;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BasicEntity { public abstract class BasicEntity {
@Comment("主键") @Comment("主键")
...@@ -50,4 +49,13 @@ public abstract class BasicEntity { ...@@ -50,4 +49,13 @@ public abstract class BasicEntity {
@Column(nullable = false) @Column(nullable = false)
@UpdateTimestamp @UpdateTimestamp
LocalDateTime updateTime; LocalDateTime updateTime;
@Comment("删除时间")
LocalDateTime deleteTime;
public interface Where {
String EXIST = " delete_time is null ";
String DELETE_SET = " set delete_time = now() where id = ? ";
}
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.exception;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Status;
import java.io.Serial;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.PropertyKey;
/**
* 业务状态异常
*
* @author Jim
* @version 0.1
* 2022/3/28 11:36
*/
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = -4226669531686389671L;
/**
* 状态码
*/
Status status;
/**
* 业务状态异常消息
*/
String message;
public BusinessException(Status status, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
this.status = status;
this.message = message;
}
public static BusinessException i18n(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
return new BusinessException(Status.BAD_REQUEST, message);
}
public static BusinessException i18n(
@NonNull Status status,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message
) {
return new BusinessException(status, message);
}
}
...@@ -30,4 +30,9 @@ public class FailStatusException extends RuntimeException { ...@@ -30,4 +30,9 @@ public class FailStatusException extends RuntimeException {
* 状态 * 状态
*/ */
Status status; Status status;
/**
* 异常消息
*/
String message;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "IdParam", description = "公共的 ID 查询参数") @Schema(name = "IdParam", description = "公共的 ID 查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -29,7 +27,7 @@ public class IdParam implements Serializable { ...@@ -29,7 +27,7 @@ public class IdParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "id", example = "1", required = true) @Parameter(description = "id", example = "1")
@NotNull(message = "id 不能为空") @NotBlank
String id; String id;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Collection;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -20,8 +19,7 @@ import lombok.experimental.FieldDefaults; ...@@ -20,8 +19,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("IdsParam") @Schema(name = "IdsParam")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -32,15 +30,16 @@ public class IdsParam implements Serializable { ...@@ -32,15 +30,16 @@ public class IdsParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8379896695668632733L; private static final long serialVersionUID = -8379896695668632733L;
@ApiModelProperty(value = "ids 多个以逗号分割", example = "1,2", required = true) @Parameter(description = "ids 多个以逗号分割", example = "1,2")
@NotEmpty(message = "ids 不能为空") @NotBlank
String ids; String ids;
/** /**
* 获取 String 类型的 ID 集合 * 获取 String 类型的 ID 集合
*
* @return ID 集合 * @return ID 集合
*/ */
public Set<String> toIds() { public Collection<Serializable> toIds() {
return Arrays.stream(this.ids.split(",")).collect(Collectors.toSet()); return Arrays.stream(this.ids.split(",")).collect(Collectors.toSet());
} }
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -16,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2022/4/27 08:53 * 2022/4/27 08:53
*/ */
@ApiModel(value = "KeywordParam", description = "公共的关键字查询参数") @Schema(name = "KeywordParam", description = "公共的关键字查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,6 +27,7 @@ public class KeywordParam implements Serializable { ...@@ -28,6 +27,7 @@ public class KeywordParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456894L; private static final long serialVersionUID = -8690942241103456894L;
@ApiModelProperty(value = "关键字", example = "hi") @Parameter(description = "关键字", example = "hi")
@NotBlank
String keyword; String keyword;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
/** /**
* 公共的分页排序查询参数 * 公共的分页排序查询参数
* eg: 支持使用 @Validated(Group.Optional.class) 来忽略校验
* *
* @author ifzm * @author ifzm
* @version 0.1 2019/3/10 16:29 * @version 0.1 2019/3/10 16:29
*/ */
@ApiModel(value = "PageParam", description = "公共的分页排序查询参数") @Schema(name = "PageParam", description = "公共的分页排序查询参数")
@Data @Data
@SuperBuilder @SuperBuilder
@NoArgsConstructor @NoArgsConstructor
...@@ -35,20 +36,20 @@ public class PageParam implements Serializable { ...@@ -35,20 +36,20 @@ public class PageParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6103761701912769946L; private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10", required = true) @Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull(message = "分页条数不能为空") @NotNull
@DecimalMin(value = "1", message = "分页条数不能小于1") @Range(min = 1, max = 100)
Integer pageSize; Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1", required = true) @Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull(message = "当前页数不能为空") @NotNull
@DecimalMin(value = "1", message = "当前页数不能小于1") @Min(1)
Integer pageNo; Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id") @Schema(description = "排序字段", defaultValue = "id", example = "id")
String sortField; String sortField;
@ApiModelProperty(value = "排序方向(ASC|DESC)", example = "DESC") @Schema(description = "排序方向(ASC|DESC)", defaultValue = "DESC", example = "DESC")
Sort.Direction sortOrder; Sort.Direction sortOrder;
/** /**
...@@ -59,14 +60,27 @@ public class PageParam implements Serializable { ...@@ -59,14 +60,27 @@ public class PageParam implements Serializable {
*/ */
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
public static Pageable toPageable(PageParam param) { public static Pageable toPageable(PageParam param) {
if (param == null) { if (param == null || Objects.isNull(param.getPageNo()) || Objects.isNull(param.getPageSize())) {
return Pageable.unpaged(); return Pageable.unpaged();
} }
return PageParam.toPageable(param.getSortField(), param.getSortOrder(), param.getPageSize(), param.getPageNo());
}
/**
* 根据参数构建分页对象
*
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param pageSize 分页大小
* @param pageNo 分页页码
* @return Pageable
*/
public static Pageable toPageable(String sortField, Sort.Direction sortOrder, Integer pageSize, Integer pageNo) {
Sort sort = Sort.unsorted(); Sort sort = Sort.unsorted();
if (Objects.nonNull(param.getSortField())) { if (Objects.nonNull(sortField)) {
sort = Sort.by(new Sort.Order(param.getSortOrder(), param.getSortField())); sort = Sort.by(new Sort.Order(sortOrder, sortField));
} }
return PageRequest.of(param.getPageNo() - 1, param.getPageSize(), sort); return PageRequest.of(pageNo - 1, pageSize, sort);
} }
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "PidParam", description = "公共的父级 ID 查询参数") @Schema(name = "PidParam", description = "公共的父级 ID 查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -29,7 +27,7 @@ public class PidParam implements Serializable { ...@@ -29,7 +27,7 @@ public class PidParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "pid", example = "0", required = true) @Parameter(description = "pid", example = "0")
@NotNull(message = "pid 不能为空") @NotBlank
String pid; String pid;
} }
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 Long 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateLongId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextId();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 String 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateStringId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextIdStr();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论