提交 280bfc17 作者: 方治民

合并分支 '3.0' 到 'beta'

3.0

查看合并请求 !14
流水线 #1993 已失败 于阶段
in 34 秒
......@@ -33,7 +33,7 @@ build-job:
before_script:
- chmod +x ./gradlew
script:
- ./gradlew app:assemble -Dskip-hooks
- ./gradlew :app:assemble -Dskip-hooks
artifacts:
# 配置构建结果过期时间
expire_in: 1 day
......@@ -54,12 +54,19 @@ deploy-job:
tags:
- YR-CD
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 构建镜像
- 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:
# 设置镜像 tag,使用 git tag 标识作为镜像 tag
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>
......@@ -16,6 +16,9 @@ COPY app/build/libs/app-0.0.1-SNAPSHOT.jar app.jar
# RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。
# RUN bash -c "touch /app.jar"
# 设置时区,解决时区问题
RUN echo "Asia/Shanghai" > /etc/timezone
# 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
EXPOSE 8081
......
......@@ -26,6 +26,7 @@
- [x] 完成项目构建,开发文档编写
- [x] [conventional-changelog](https://www.cnblogs.com/mengfangui/p/12634845.html)
- [x] 用户及权限模块(目录/菜单/按钮),预览初始化权限配置 [SQL 脚本](./basic-auth/src/main/resources/init-test-mysql.sql)
- [x] 通用文件上传模块
- [x] 通用文件上传模块,支持对图片/PDF/MP3/MP4 等文件进行预处理
- [x] WebSocket 模块
- [ ] 通用字典管理模块
- [ ] XXL-JOB 定时任务模块
group = 'com.yiring'
version = '0.0.1-SNAPSHOT'
processResources {
filesMatching('application.yml') {
expand(project.properties)
}
}
bootJar {
enabled = true
}
dependencies {
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-validation'
// 💬 Mock/Test Env
runtimeOnly 'com.h2database:h2'
// 💬 Prod/Dev Env
......@@ -22,33 +28,44 @@ dependencies {
implementation project(":basic-common:core")
implementation project(":basic-common:util")
// Optional: I18n 消息, 包括参数校验、失败的请求提示等
implementation project(":basic-common:i18n")
// Optional: Redis
implementation project(":basic-common:redis")
// Optional: 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
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
implementation project(":basic-common:minio")
// FIX: minio dep
implementation "io.minio:minio:${minioVersion}"
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
// Optional: MyBatis Plus
implementation "com.baomidou:mybatis-plus-boot-starter:${mybatisPlusVersion}"
// Optional: 扩展实现在文件上传时对文件进行预处理,依赖 Minio 模块
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types
// hibernate-types-55
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.app;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@MapperScan(basePackages = Application.BASE_PACKAGES + ".app.mapper")
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@EntityScan(
basePackageClasses = { Application.class, Jsr310JpaConverters.class },
basePackages = Application.BASE_PACKAGES
)
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES)
public class Application {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.app.config;
import cn.dev33.satoken.exception.NotLoginException;
import com.yiring.app.constant.Code;
import com.yiring.app.exception.CodeException;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.FailStatusException;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.validation.BindException;
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.core.annotation.Order;
import org.springframework.http.HttpStatus;
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;
* 2017年11月30日 上午11:36:31
*/
@Slf4j
@ControllerAdvice
@ResponseBody
@Order
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
/**
* 参数校验异常
*
* @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() {}
final I18n i18n;
/**
* 其他异常
......@@ -110,9 +32,9 @@ public class GlobalExceptionHandler {
* @param e 异常信息
* @return 统一的500异常信息 {@link Status#INTERNAL_SERVER_ERROR
*/
@ExceptionHandler(value = Exception.class)
public Result<String> defaultErrorHandler(Exception e, HttpServletResponse response) {
response.setStatus(Status.INTERNAL_SERVER_ERROR.value());
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public Result<String> defaultErrorHandler(Exception e) {
log.error(e.getMessage(), 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. */
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.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment;
/**
* 测试表
* 用户扩展
*
* @author Jim
* @version 0.1
* 2022/4/15 18:34
* 2022/7/13 10:59
*/
@Getter
......@@ -29,21 +28,28 @@ import org.hibernate.annotations.Comment;
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Entity
@TableName("TEST_TABLE")
@Table(name = "TEST_TABLE")
@Comment("测试表")
public class TestTable implements Serializable {
@Table(name = "SYS_USER_EXTENSION", uniqueConstraints = @UniqueConstraint(columnNames = "user_id"))
@Comment("用户扩展表")
public class UserExtension extends BasicEntity implements Serializable {
@Serial
private static final long serialVersionUID = -6168070383092874608L;
private static final long serialVersionUID = -1157047754883351972L;
@Comment("主键")
@Id
String id;
@Comment("用户")
@OneToOne
@JoinColumn(nullable = false, name = "user_id")
User user;
@Comment("姓名")
String name;
@Comment("性别")
Integer gender;
@Comment("年龄")
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. */
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.yiring.app.constant.Code;
import com.yiring.app.domain.TestTable;
import com.yiring.app.exception.CodeException;
import com.yiring.app.mapper.TestTableMapper;
import com.yiring.app.domain.user.UserExtension;
import com.yiring.app.domain.user.UserExtensionRepository;
import com.yiring.app.vo.user.UserExtensionVo;
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.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
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.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.Example;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 示例接口
*
* @author Jim
*/
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = 0)
@Api(tags = "示例", description = "Example")
@Tag(name = "示例", description = "Example")
@RequestMapping("/example/")
@RestController
@RequiredArgsConstructor
public class ExampleController {
String text = "😎 Hello World";
final I18n i18n;
final Auths auths;
final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
@Operation(summary = "Hello World")
@GetMapping
public Result<String> hello() {
return Result.ok(text);
return Result.ok("example.hello");
}
/**
* 测试失败自定义状态信息输出
*/
@Operation(summary = "测试失败")
@GetMapping("fail")
public Result<String> fail() {
throw new CodeException(Code.FAIL);
throw BusinessException.i18n("Code.1");
}
@SaCheckLogin
@Operation(summary = "分页条件查询")
@GetMapping("page")
public Result<PageVo<String>> page(@Valid PageParam pageParam) {
log.info("PageParam: {}", pageParam);
public Result<PageVo<String>> page(@ParameterObject @Validated PageParam param) {
log.debug("PageParam: {}", param);
String text = i18n.get("example.hello");
List<String> data = Arrays.asList(text.split(" "));
PageVo<String> vo = PageVo.build(data, data.size());
return Result.ok(vo);
}
@ApiOperation(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
@Operation(summary = "JSON 传参")
@PostMapping("json")
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");
FileUtils.download(response, resource.getFile());
}
@ApiOperation("测试 MyBatis Plus 查询")
@GetMapping("test")
public Result<String> test() {
List<TestTable> tests = SpringUtil.getBean(TestTableMapper.class).selectList(null);
return Result.ok(tests.stream().map(TestTable::getName).reduce((a, b) -> a + "," + b).orElse(""));
@Operation(summary = "文件上传")
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(
@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file,
@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:
host: 192.168.0.156
prod: false
extra:
username: admin
password: 123456
# ----------------------------------------------
spring:
datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
url: jdbc:postgresql://${env.host}:5432/basic_app
username: ${env.extra.username}
password: ${env.extra.password}
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true
hibernate:
ddl-auto: update
show-sql: false
show-sql: true
properties:
hibernate:
format_sql: true
redis:
database: 5
host: ${env.host}
port: 6379
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
schema_update:
unique_constraint_strategy: RECREATE_QUIETLY
# https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827
jakarta:
persistence:
sharedCache:
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:
enable: true
basic:
enable: false
username: admin
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
setting:
enableOpenApi: false
language: zh_cn
enableOpenApi: true
enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio
minio:
access-key: minioadmin
secret-key: minioadmin
end-point: "http://${env.host}:18100"
bucket: basic
domain: ${minio.endpoint}/${minio.bucket}
bucket: public
domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging:
level:
# 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:
hibernate:
# 关闭 hibernate-types banner 日志信息
types.print.banner: false
data:
redis:
repositories:
enabled: false
logging:
level:
# 关闭接口扫描 CachingOperationNameGenerator 日志
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:
host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring:
datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
open-in-view: true
......@@ -20,33 +24,30 @@ spring:
database: 5
host: ${env.host}
port: 6379
password: 123456
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
password: ${env.extra.password}
# knife4j
knife4j:
enable: true
basic:
enable: false
username: admin
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
setting:
enableOpenApi: false
enableDebug: true
# minio
minio:
access-key: minioadmin
secret-key: minioadmin
access-key: ${env.extra.username}
secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100"
bucket: basic
domain: ${minio.endpoint}/${minio.bucket}
bucket: public
domain: ${minio.end-point}
logging:
level:
# sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# 环境变量
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:
datasource:
url: jdbc:postgresql://${env.host}:5432/basic_app
username: admin
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
jpa:
database-platform: org.hibernate.dialect.PostgreSQL10Dialect
database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true
hibernate:
ddl-auto: update
......@@ -16,37 +25,60 @@ spring:
properties:
hibernate:
format_sql: true
redis:
database: 5
host: ${env.host}
port: 6379
password: 123456
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
schema_update:
unique_constraint_strategy: RECREATE_QUIETLY
# https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827
jakarta:
persistence:
sharedCache:
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:
enable: true
basic:
enable: false
username: admin
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
setting:
language: zh_cn
enableOpenApi: true
enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio
minio:
access-key: minioadmin
secret-key: minioadmin
end-point: "http://${env.host}:18100"
bucket: basic
domain: ${minio.endpoint}/${minio.bucket}
bucket: public
domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging:
level:
# sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# ----------------------------------------------
# 环境变量
env:
host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring:
datasource:
......@@ -20,34 +24,30 @@ spring:
database: 5
host: ${env.host}
port: 6379
password: 123456
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
password: ${env.extra.password}
# knife4j
knife4j:
enable: true
basic:
enable: false
username: admin
password: 123456
username: ${env.extra.username}
password: ${env.extra.password}
setting:
enableOpenApi: true
enableDebug: true
# minio
minio:
access-key: minioadmin
secret-key: minioadmin
access-key: ${env.extra.username}
secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100"
bucket: basic
domain: ${minio.endpoint}/${minio.bucket}
bucket: public
domain: ${minio.end-point}
logging:
level:
# sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
app:
version: ${version}
server:
port: 8081
servlet:
context-path: /api
tomcat:
max-http-form-post-size: 20MB
spring:
servlet:
# 文件上传大小限制
multipart:
max-file-size: 10MB
max-request-size: 30MB
application:
name: "basic-api-app"
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1048MB
profiles:
include: auth, conf-patch
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 {
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// 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}"
// Sa-Token 整合 Redis (使用 jackson 序列化方式)
implementation 'org.apache.commons:commons-pool2'
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types
// hibernate-types-55
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
// hypersistence-utils-hibernate-60
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. */
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.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.util.Auths;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
......@@ -22,24 +23,24 @@ public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的路由拦截器
// 注册 Sa-Token 路由拦截器
// 可搭配使用注解实现鉴权: https://sa-token.dev33.cn/doc.html#/use/at-check
registry
.addInterceptor(
new SaRouteInterceptor((req, res, handler) -> {
new SaInterceptor(handle -> {
// 登录认证 -- 拦截所有路由,并排除 /auth/** 用于开放授权相关, 以及 swagger 相关
SaRouter
.match("/**")
.notMatchMethod(SaHttpMethod.OPTIONS.name())
// 实现用户权限相关后应移除下行代码
// TODO
// .notMatch("/**")
// 示例接口
.notMatch("/example/**")
// 授权相关接口(登录、登出、注册等)
.notMatch("/auth/**")
.notMatch("/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css")
.notMatch("/v2/api-docs/**", "/v3/api-docs/**", "/swagger-resources/**")
.check(r -> StpUtil.checkLogin());
// 管理员权限才可访问的路由地址
SaRouter.match("/sys/**", r -> StpUtil.checkRoleOr(Auths.ADMIN_ROLES.toArray(new String[0])));
})
)
.addPathPatterns("/**");
......
......@@ -6,13 +6,15 @@ import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 获取登录用户权限信息实现
......@@ -22,11 +24,12 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37
*/
@Transactional(readOnly = true)
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
......@@ -36,6 +39,7 @@ public class StpInterfaceImpl implements StpInterface {
.stream()
.map(Role::getPermissions)
.flatMap(Set::stream)
.distinct()
.map(Permission::getUid)
.collect(Collectors.toList());
}
......@@ -48,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface {
/**
* 根据 id 获取用户信息
*
* @param loginId 登录 ID
* @return 用户信息
*/
private User getUser(Object loginId) {
public User getUser(Object loginId) {
String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) {
throw new RuntimeException("用户不存在");
throw Status.NOT_FOUND.exception("Code.1000");
}
return optional.get();
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.permission;
import com.alibaba.fastjson.JSONObject;
import com.vladmihalcea.hibernate.type.json.JsonType;
import static com.yiring.auth.domain.permission.Permission.DELETE_SQL;
import static com.yiring.auth.domain.permission.Permission.TABLE_NAME;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.common.domain.BasicEntity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
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;
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@TypeDef(name = "json", typeClass = JsonType.class)
@Table(
name = "SYS_PERMISSION",
indexes = { @Index(columnList = "type"), @Index(columnList = "pid"), @Index(columnList = "tree") }
name = TABLE_NAME,
indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Permission.Fields.type),
@Index(columnList = Permission.Fields.pid),
@Index(columnList = Permission.Fields.tree),
}
)
@Comment("系统权限")
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
private static final long serialVersionUID = -2001221843529000953L;
@Comment("类型(MENU: 菜单, BUTTON: 按钮)")
@Column(nullable = false)
@Enumerated(EnumType.STRING)
Type type;
......@@ -49,10 +65,11 @@ public class Permission extends BasicEntity implements Serializable {
Integer serial;
@Comment("标识")
@Column(unique = true, nullable = false)
@Column(nullable = false)
String uid;
@Comment("名称")
@Column(nullable = false)
String name;
@Comment("路径")
......@@ -83,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable {
* 可用于扩展一些前端可能用到的路由参数
*/
@Comment("扩展元数据信息")
@org.hibernate.annotations.Type(type = "json")
@org.hibernate.annotations.Type(JsonType.class)
@Column(columnDefinition = "json")
JSONObject meta;
......@@ -107,10 +124,11 @@ public class Permission extends BasicEntity implements Serializable {
/**
* 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整
*
* @return JSON 格式 Meta 元数据
*/
public JSONObject getMetaJson() {
JSONObject meta = new JSONObject(true);
JSONObject meta = new JSONObject();
meta.put("title", this.name);
meta.put("icon", this.icon);
meta.put("orderNo", this.serial);
......
/* (C) 2022 YiRing, Inc. */
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.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
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;
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = @Index(columnList = BasicEntity.Fields.deleteTime),
uniqueConstraints = { @UniqueConstraint(columnNames = { Role.Fields.uid, BasicEntity.Fields.deleteTime }) }
)
@Comment("系统角色")
@Table(name = "SYS_ROLE", indexes = { @Index(columnList = "uid", unique = true) })
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
private static final long serialVersionUID = 910404402503275957L;
......@@ -44,8 +60,12 @@ public class Role extends BasicEntity implements Serializable {
String uid;
@Comment("名称")
@Column(nullable = false)
String name;
@Comment("是否启用")
Boolean enable;
@JsonIgnore
@Builder.Default
@Comment("权限集合")
......
......@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* 角色接口
......@@ -10,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
* @author ifzm
* 2018/9/4 8:49
*/
@Repository
public interface RoleRepository extends JpaRepository<Role, Serializable> {}
......@@ -4,17 +4,19 @@ package com.yiring.auth.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.role.Role;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
......@@ -32,45 +34,49 @@ import org.hibernate.annotations.Where;
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Where(clause = "deleted = false")
@SQLDelete(sql = User.DELETE_SQL)
@SQLDeleteAll(sql = User.DELETE_SQL)
@Where(clause = User.Where.EXIST)
@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("系统用户")
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
private static final long serialVersionUID = -5787847701210907511L;
@Comment("头像")
String avatar;
@Comment("真实姓名")
String realName;
@Comment("用户名")
@Column(unique = true)
String username;
@Comment("手机号")
@Column(unique = true)
String mobile;
@Comment("邮箱")
@Column(unique = true)
String email;
@Comment("密码")
String password;
@Comment("简介")
String introduction;
@Comment("头像")
String avatar;
@Comment("是否启用")
Boolean enabled;
@Comment("是否删除")
Boolean deleted;
@JsonIgnore
@Builder.Default
@Comment("角色集合")
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("LoginParam")
@Schema(name = "LoginParam")
@Data
@Builder
@NoArgsConstructor
......@@ -27,11 +27,11 @@ public class LoginParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "账号(支持用户名/手机号/邮箱)", example = "admin", required = true)
@Parameter(description = "账号(支持用户名/手机号/邮箱)", example = "admin")
@NotEmpty(message = "账号不能为空")
String account;
@ApiModelProperty(value = "密码", example = "123456", required = true)
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
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.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("RegisterParam")
@Schema(name = "RegisterParam")
@Data
@Builder
@NoArgsConstructor
......@@ -28,32 +28,29 @@ public class RegisterParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "用户名", example = "admin", required = true)
@Parameter(description = "用户名", example = "admin")
@NotEmpty(message = "用户名不能为空")
String username;
@ApiModelProperty(value = "密码", example = "123456", required = true)
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
@ApiModelProperty(value = "真实姓名", example = "管理员", required = true)
@Parameter(description = "真实姓名", example = "管理员")
@NotEmpty(message = "真实姓名不能为空")
String realName;
@ApiModelProperty(value = "手机号", example = "13012345678", required = true)
@Parameter(description = "手机号", example = "13012345678")
@NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确")
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;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
@Parameter(description = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "简介", example = "平台管理员")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
@Parameter(description = "是否启用", example = "true")
Boolean enable;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 公共的可选父级 ID 查询参数
* 安全校验参数
*
* @author ifzm
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "OptionalPidParam", description = "公共的可选父级 ID 查询参数")
@Valid
@Schema(name = "SafeParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class OptionalPidParam implements Serializable {
public class SafeParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456893L;
private static final long serialVersionUID = 9106494470582579138L;
@ApiModelProperty(value = "pid", example = "0")
String pid;
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import com.yiring.common.validation.group.Group;
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.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 权限信息入参类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionParam")
@Schema(name = "PermissionParam")
@Data
@Builder
@NoArgsConstructor
......@@ -31,43 +34,47 @@ public class PermissionParam implements Serializable {
@Serial
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 = "权限类型不能为空")
Permission.Type type;
@ApiModelProperty(value = "序号", example = "1")
@Schema(description = "序号", example = "1")
Integer serial;
@ApiModelProperty(value = "标识", example = "Dashboard", required = true)
@Schema(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空")
String uid;
@ApiModelProperty(value = "名称", example = "Dashboard", required = true)
@Schema(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空")
String name;
@ApiModelProperty(value = "路径", example = "/dashboard")
@Schema(description = "路径", example = "/dashboard")
String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench")
@Schema(description = "重定向", example = "/dashboard/workbench")
String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT")
@Schema(description = "组件", example = "LAYOUT")
String component;
@ApiModelProperty(value = "图标", example = "ion:grid-outline")
@Schema(description = "图标", example = "ion:grid-outline")
String icon;
@ApiModelProperty(value = "是否隐藏", example = "false")
@Schema(description = "是否隐藏", example = "false")
Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true")
@Schema(description = "是否启用", example = "true")
Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0")
@Schema(description = "父级ID", example = "0")
@Builder.Default
String pid = "0";
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta;
@Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.role;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import com.yiring.common.validation.group.Group;
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.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 角色信息入参类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@ApiModel("RoleParam")
@Schema(name = "RoleParam")
@Data
@Builder
@NoArgsConstructor
......@@ -27,11 +30,15 @@ public class RoleParam implements Serializable {
@Serial
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 = "角色标识不能为空")
String uid;
@ApiModelProperty(value = "名称", example = "管理员", required = true)
@Parameter(description = "名称", example = "管理员")
@NotEmpty(message = "角色名称不能为空")
String name;
}
......@@ -6,10 +6,11 @@ import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Resource;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
......@@ -22,14 +23,20 @@ import org.springframework.stereotype.Component;
@SuppressWarnings("unused")
@Component
@RequiredArgsConstructor
public class Auths {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
/**
* 管理员角色标识
*/
public static final List<String> ADMIN_ROLES = List.of("admin", "super-admin", "platform-admin", "data-admin");
/**
* 根据 Token 获取用户信息
* 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED}
*
* @param token token
* @return 用户信息
*/
......@@ -61,4 +68,50 @@ public class Auths {
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. */
package com.yiring.auth.util;
import com.sun.istack.Nullable;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.util.Commons;
import jakarta.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
......@@ -28,6 +28,7 @@ public class Permissions {
/**
* 将角色集合转换成 Vo 集合
*
* @param roles 角色集合
* @return vos
*/
......@@ -40,6 +41,7 @@ public class Permissions {
/**
* 将权限集合转换成菜单树
*
* @param permissions 权限集合
* @return 菜单树
*/
......@@ -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 集合
*
* @param permissions 权限集合
* @return vos
*/
......@@ -92,6 +118,7 @@ public class Permissions {
/**
* 提取角色集合含有的权限去重结果
*
* @param roles 角色集合
* @return 权限集合
*/
......@@ -101,14 +128,16 @@ public class Permissions {
.stream()
.map(Role::getPermissions)
.flatMap(Set::stream)
.distinct()
.sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length)))
.collect(Collectors.toList());
}
/**
* 根据 pid 构建树状权限集合
*
* @param permissions 权限集合
* @param pid 权限父级 ID
* @param pid 权限父级 ID
* @return 树状权限集合
*/
public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.vo.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
......@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2019/5/28 22:11
*/
@ApiModel("LoginVo")
@Schema(name = "LoginVo")
@Data
@Builder
@NoArgsConstructor
......@@ -27,9 +26,9 @@ public class LoginVo implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456896L;
@ApiModelProperty(value = "用户 ID", example = "1")
@Schema(description = "用户 ID", example = "1")
String userId;
@ApiModelProperty(value = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
@Schema(description = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
String token;
}
/* (C) 2022 YiRing, Inc. */
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.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -14,13 +13,14 @@ import lombok.experimental.FieldDefaults;
/**
* 菜单输出类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("MenuVo")
@Schema(name = "MenuVo")
@Data
@Builder
@NoArgsConstructor
......@@ -37,21 +37,24 @@ public class MenuVo implements Serializable {
@JsonIgnore
String pid;
@ApiModelProperty(value = "名称", example = "Dashboard")
@Schema(description = "唯一标识", example = "Dashboard")
String uid;
@Schema(description = "名称", example = "Dashboard")
String name;
@ApiModelProperty(value = "路径", example = "/dashboard")
@Schema(description = "路径", example = "/dashboard")
String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench")
@Schema(description = "重定向", example = "/dashboard/workbench")
String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT")
@Schema(description = "组件", example = "LAYOUT")
String component;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
@Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
@Schema(description = "子权限")
List<MenuVo> children;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults;
/**
* 权限输出类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionVo")
@Schema(name = "PermissionVo")
@Data
@Builder
@NoArgsConstructor
......@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable {
@Serial
private static final long serialVersionUID = -9139328772148985141L;
@ApiModelProperty(value = "主键", example = "1")
@Parameter(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "权限类型", example = "MENU")
@Parameter(description = "权限类型", example = "MENU")
Permission.Type type;
@ApiModelProperty(value = "序号", example = "1")
@Parameter(description = "序号", example = "1")
Integer serial;
@ApiModelProperty(value = "标识", example = "home")
@Parameter(description = "标识", example = "home")
String uid;
@ApiModelProperty(value = "名称", example = "主页")
@Parameter(description = "名称", example = "主页")
String name;
@ApiModelProperty(value = "路径", example = "/")
@Parameter(description = "路径", example = "/")
String path;
@ApiModelProperty(value = "组件", example = "/home")
@Parameter(description = "组件", example = "/home")
String component;
@ApiModelProperty(value = "图标", example = "menu")
@Parameter(description = "图标", example = "menu")
String icon;
@ApiModelProperty(value = "是否隐藏", example = "false")
@Parameter(description = "是否隐藏", example = "false")
Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true")
@Parameter(description = "是否启用", example = "true")
Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0")
@Parameter(description = "父级ID", example = "0")
String pid;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
@Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
@Parameter(description = "子权限")
List<PermissionVo> children;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.role;
import com.yiring.auth.vo.permission.PermissionVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults;
/**
* 角色响应类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@ApiModel("RoleVo")
@Schema(name = "RoleVo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,15 +28,15 @@ public class RoleVo implements Serializable {
@Serial
private static final long serialVersionUID = -9154497137563970840L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "标识", example = "admin")
@Schema(description = "标识", example = "admin")
String uid;
@ApiModelProperty(value = "名称", example = "系统管理员")
@Schema(description = "名称", example = "系统管理员")
String name;
@ApiModelProperty("权限")
@Schema(description = "权限")
List<PermissionVo> permissions;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
......@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserInfo")
@Schema(name = "UserInfo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,25 +28,28 @@ public class UserInfoVo implements Serializable {
@Serial
private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "手机号", example = "15616260195")
String mobile;
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "介绍", example = "系统管理员")
@Schema(description = "介绍", example = "系统管理员")
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;
@ApiModelProperty("角色")
@Schema(description = "角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
@Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
......@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserInfo")
@Schema(name = "UserInfo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable {
@Serial
private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "介绍", example = "系统管理员")
@Schema(description = "介绍", example = "系统管理员")
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;
@ApiModelProperty("角色")
@Schema(description = "角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
@Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.user;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
......@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserVo")
@Schema(name = "UserVo")
@Data
@Builder
@NoArgsConstructor
......@@ -26,39 +26,39 @@ public class UserVo implements Serializable {
@Serial
private static final long serialVersionUID = -2184378273450466835L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "手机号", example = "13012345678")
@Schema(description = "手机号", example = "13012345678")
String mobile;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
@Schema(description = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "职称", example = "系统管理员")
@Schema(description = "职称", example = "系统管理员")
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;
@ApiModelProperty(value = "是否启用", example = "true")
@Schema(description = "是否启用", example = "true")
Boolean enabled;
@ApiModelProperty(value = "是否删除", example = "false")
@Schema(description = "是否删除", example = "false")
Boolean deleted;
@ApiModelProperty(value = "最后登录IP地址", example = "127.0.0.1")
@Schema(description = "最后登录IP地址", example = "127.0.0.1")
String lastLoginIp;
@ApiModelProperty(value = "最后登录时间", example = "2022-10-24 10:24:00")
@Schema(description = "最后登录时间", example = "2022-10-24 10:24:00")
LocalDateTime lastLoginTime;
@ApiModelProperty(value = "最后登录时间", example = "2022-01-01 00:00:00")
@Schema(description = "最后登录时间", example = "2022-01-01 00:00:00")
LocalDateTime createTime;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.auth;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil;
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.UserRepository;
import com.yiring.auth.param.auth.LoginParam;
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.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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 jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Example;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
......@@ -37,100 +41,124 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -99)
@Api(tags = "身份认证", description = "Auth")
@Tag(
name = "Auth",
description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@RestController
@RequestMapping("/auth/")
@RequiredArgsConstructor
public class AuthController {
@Resource
UserRepository userRepository;
final Auths auths;
final UserRepository userRepository;
@ApiOperation(value = "注册")
@PostMapping("register")
public Result<String> register(@Valid RegisterParam param) {
@Operation(summary = "注册")
@PostMapping(value = "register")
public Result<String> register(@ParameterObject @Validated RegisterParam param) {
// 检查用户名是否存在
long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build()));
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()));
if (count > 0) {
return Result.no(Status.BAD_REQUEST, "手机号已存在");
throw BusinessException.i18n("Code.100001");
}
// 检查邮箱是否存在
if (StrUtil.isNotBlank(param.getEmail())) {
count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build()));
if (count > 0) {
return Result.no(Status.BAD_REQUEST, "邮箱已存在");
throw BusinessException.i18n("Code.100002");
}
}
// 构建用户信息写入数据库
User user = User
.builder()
.introduction(param.getIntroduction())
.avatar(param.getAvatar())
.mobile(param.getMobile())
.realName(param.getRealName())
.username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable())
.deleted(Boolean.FALSE)
.createTime(LocalDateTime.now())
.build();
userRepository.saveAndFlush(user);
return Result.ok();
}
@ApiOperation(value = "登录")
@Operation(summary = "登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid LoginParam param, HttpServletRequest request) {
String details = "账号密码错误";
public Result<LoginVo> login(@ParameterObject @Validated LoginParam param, HttpServletRequest request) {
// 查询用户信息是否匹配
User user = userRepository.findByAccount(param.getAccount());
if (user == null) {
return Result.no(Status.BAD_REQUEST, details);
throw BusinessException.i18n("Code.100003");
}
// 检查密码
String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) {
return Result.no(Status.BAD_REQUEST, details);
throw BusinessException.i18n("Code.100003");
}
// 检查用户是否已被删除
if (!Boolean.FALSE.equals(user.getDeleted())) {
return Result.no(Status.FORBIDDEN, "用户被禁用, 请联系管理员");
if (user.getDeleteTime() != null) {
throw BusinessException.i18n("Code.100004");
}
// 检查用户是否被允许登录
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.setLastLoginTime(LocalDateTime.now());
userRepository.saveAndFlush(user);
// 登录
StpUtil.login(user.getId());
// 构建用户所需信息
LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build();
return Result.ok(vo);
}
@ApiOperation(value = "登出")
@Operation(summary = "检查登录")
@GetMapping("valid")
public Result<Boolean> valid() {
return Result.ok(StpUtil.isLogin());
}
@Operation(summary = "登出")
@GetMapping("logout")
public Result<String> logout() {
StpUtil.logout();
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. */
package com.yiring.auth.web.permission;
package com.yiring.auth.web.sys.permission;
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.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam;
......@@ -11,26 +9,27 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.PermissionVo;
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.OptionalPidParam;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example;
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;
import org.springframework.web.bind.annotation.*;
/**
* 系统权限管理控制器
......@@ -42,60 +41,56 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -97)
@Api(tags = "权限管理", description = "Permission")
@Tag(
name = "权限管理",
description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@RestController
@RequestMapping("/manage/permission/")
@RequestMapping("/sys/permission/")
@RequiredArgsConstructor
public class PermissionController {
@Resource
PermissionRepository permissionRepository;
final PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@Valid PermissionParam param) {
public Result<String> add(@RequestBody @Validated({ Group.Add.class }) PermissionParam param) {
if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复");
throw BusinessException.i18n("Code.1001");
}
Permission entity = new Permission();
BeanUtils.copyProperties(param, entity);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
save(entity, param);
return Result.ok();
}
@ApiOperation(value = "修改")
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@Valid PermissionParam param, @Valid IdParam idParam) {
Optional<Permission> optional = permissionRepository.findById(idParam.getId());
public Result<String> modify(@RequestBody @Validated({ Group.Edit.class }) PermissionParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
Permission entity = optional.get();
if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复
if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复");
throw BusinessException.i18n("Code.1001");
}
}
BeanUtils.copyProperties(param, entity);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
save(entity, param);
return Result.ok();
}
@ApiOperation(value = "删除")
@PostMapping("deleted")
public Result<String> deleted(@Valid IdParam param) {
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
Permission entity = optional.get();
......@@ -103,31 +98,33 @@ public class PermissionController {
return Result.ok();
}
@ApiOperation(value = "查询")
@Operation(summary = "查询")
@GetMapping("find")
public Result<PermissionVo> find(@Valid IdParam param) {
public Result<PermissionVo> find(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
Permission permission = optional.get();
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);
}
@ApiOperation(value = "分页查询")
@Operation(summary = "分页查询")
@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));
List<PermissionVo> data = Permissions.toPermissionVos(page.toList());
PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@ApiOperation(value = "树结构查询")
@Operation(summary = "树结构查询")
@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<PermissionVo> vos = Permissions.toTree(
permissions,
......@@ -138,6 +135,7 @@ public class PermissionController {
/**
* 根据父级 ID 获取当前树节点标识
*
* @param pid 父级 ID
* @return 树节点标识
*/
......@@ -152,6 +150,7 @@ public class PermissionController {
/**
* 检查是否存在已有相同标识的权限
*
* @param uid 权限标识
* @return 是否存在
*/
......@@ -159,4 +158,16 @@ public class PermissionController {
Permission entity = Permission.builder().uid(uid).build();
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. */
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.PermissionRepository;
import com.yiring.auth.domain.role.Role;
......@@ -11,17 +10,21 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.role.RoleVo;
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.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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 javax.annotation.Resource;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
......@@ -41,24 +44,24 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -96)
@Api(tags = "角色管理", description = "Role")
@Tag(
name = "角色管理",
description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@RestController
@RequestMapping("/manage/role/")
@RequestMapping("/sys/role/")
@RequiredArgsConstructor
public class RoleController {
@Resource
RoleRepository roleRepository;
final RoleRepository roleRepository;
final PermissionRepository permissionRepository;
@Resource
PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@Valid RoleParam param) {
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) RoleParam param) {
if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复");
throw BusinessException.i18n("Code.1002");
}
Role entity = new Role();
......@@ -67,19 +70,19 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "修改")
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@Valid RoleParam param, @Valid IdParam idParam) {
Optional<Role> optional = roleRepository.findById(idParam.getId());
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) RoleParam param) {
Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
Role entity = optional.get();
if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复
if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复");
throw BusinessException.i18n("Code.1002");
}
}
......@@ -88,21 +91,20 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "分配权限")
@Operation(summary = "分配权限")
@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());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
// 查询权限集合
Set<String> ids = idsParam.toIds();
Set<Permission> permissions = permissionRepository
.findAll()
.stream()
.filter(permission -> ids.contains(permission.getId()))
.collect(Collectors.toSet());
Collection<Serializable> ids = idsParam.toIds();
Set<Permission> permissions = new HashSet<>(permissionRepository.findAllById(ids));
Role entity = optional.get();
entity.setPermissions(permissions);
......@@ -110,25 +112,20 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "删除")
@PostMapping("deleted")
public Result<String> deleted(@Valid IdParam param) {
Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
}
Role entity = optional.get();
roleRepository.delete(entity);
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<Role> roles = roleRepository.findAllById(param.toIds());
roleRepository.deleteAll(roles);
return Result.ok();
}
@ApiOperation(value = "查询")
@Operation(summary = "查询")
@GetMapping("find")
public Result<RoleVo> find(@Valid IdParam param) {
public Result<RoleVo> find(@ParameterObject @Validated IdParam param) {
Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
throw Status.NOT_FOUND.exception();
}
Role entity = optional.get();
......@@ -138,16 +135,16 @@ public class RoleController {
return Result.ok(vo);
}
@ApiOperation(value = "分页查询")
@Operation(summary = "分页查询")
@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));
List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet()));
PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@ApiOperation(value = "选项查询")
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<RoleVo>> selector() {
List<Role> roles = roleRepository.findAll();
......@@ -157,6 +154,7 @@ public class RoleController {
/**
* 检查是否存在已有相同标识的角色
*
* @param uid 角色标识
* @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. */
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.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.util.Auths;
import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.MenuVo;
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.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.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
......@@ -46,39 +32,35 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -95)
@Api(tags = "用户管理", description = "User")
@Tag(
name = "用户信息",
description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@RestController
@RequestMapping("/user/")
public class UserController {
@RequiredArgsConstructor
public class UserViewController {
@Resource
Auths auths;
final Auths auths;
@Resource
UserRepository userRepository;
@Resource
RoleRepository roleRepository;
@ApiOperation(value = "获取登录用户信息")
@Operation(summary = "获取登录用户信息")
@GetMapping("getUserInfo")
public Result<UserInfoVo> getUserInfo() {
User user = auths.getLoginUser();
UserInfoVo userInfoVo = UserInfoVo
.builder()
.userId(user.getId())
.mobile(user.getMobile())
.username(user.getUsername())
.realName(user.getRealName())
.avatar(user.getAvatar())
.desc(user.getIntroduction())
.roles(Permissions.toRoleVos(user.getRoles()))
.build();
return Result.ok(userInfoVo);
}
@ApiOperation(value = "获取用户菜单")
@Operation(summary = "获取用户菜单")
@GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() {
User user = auths.getLoginUser();
......@@ -91,7 +73,7 @@ public class UserController {
return Result.ok((ArrayList<MenuVo>) vos);
}
@ApiOperation(value = "获取用户权限")
@Operation(summary = "获取用户权限")
@GetMapping("getPermCode")
public Result<ArrayList<String>> getPermCode() {
User user = auths.getLoginUser();
......@@ -99,36 +81,4 @@ public class UserController {
List<String> codes = permissions.stream().map(Permission::getUid).collect(Collectors.toList());
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:
# token名称 (同时也是cookie名称)
token-name: Authorization
token-name: App-Token
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# 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 {
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-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-validation'
// 本地依赖
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-extra
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// JTS 几何对象操作库
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
implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") {
exclude group: 'com.fasterxml.jackson.core'
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. */
package com.yiring.common.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.util.Commons;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
......@@ -34,6 +35,11 @@ public class RequestAspect {
@Value("${debug}")
Boolean debug;
/**
* 白名单(忽略)
*/
List<String> IGNORE_LIST = List.of("/swagger-resources", "/error", "/v3/api-docs");
@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)"
)
......@@ -42,20 +48,33 @@ public class RequestAspect {
@Around("apiPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest();
// 放行白名单
for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) {
return point.proceed();
}
}
// 计算接口执行耗时
long start = System.currentTimeMillis();
Object result = point.proceed();
long end = System.currentTimeMillis();
// 计算接口处理消耗时间,格式化
String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME);
String times = String.format("%.3fs", (double) (end - start) / 1000);
// Print Request Log (Optional Replace: MDC)
// 获取接口请求扩展信息,Header, Params
String extra = "";
if (Boolean.TRUE.equals(debug)) {
String headers = JSONObject.toJSONString(ServletUtil.getHeaderMap(request), SerializerFeature.PrettyFormat);
String params = JSONObject.toJSONString(ServletUtil.getParamMap(request), SerializerFeature.PrettyFormat);
String headers = JSONObject.toJSONString(
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("\nParams: %s", params);
if (result instanceof Result) {
......@@ -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(
"[Request] Method: {}, URL: {}, IP: {}, Times: {}{}",
"[Request] Method: {}, URL: {}, Status: {}, IP: {}, Times: {}{}",
request.getMethod(),
request.getRequestURL(),
status,
Commons.getClientIpAddress(request),
times,
extra
);
if (result instanceof Result) {
((Result<?>) result).setTimestamp(timestamp);
((Result<?>) result).setTimes(times);
}
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. */
package com.yiring.common.config;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
......@@ -17,12 +19,71 @@ import org.springframework.context.annotation.Configuration;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration
@Configuration("env.config")
@ConfigurationProperties(prefix = "env")
public class EnvConfig {
public class EnvConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1017213697767634790L;
/**
* 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;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.n52.jackson.datatype.jts.JtsModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@RequiredArgsConstructor
public class JacksonConfig {
@Resource
JavaTimeModule javaTimeModule;
final JavaTimeModule javaTimeModule;
@Bean
public ObjectMapper objectMapper() {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.PropertyKey;
/**
* 标准的响应对象(所有的接口响应内容格式都应该是一致的)
......@@ -21,63 +23,70 @@ import lombok.extern.slf4j.Slf4j;
*/
@SuppressWarnings({ "unchecked", "unused" })
@ApiModel("Result")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Slf4j
@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Result<T extends Serializable> implements Serializable {
public class Result<T> implements Serializable {
@Serial
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;
/**
* 接口耗时(单位:秒)通常在调试阶段出现
*/
@ApiModelProperty(value = "耗时", example = "0.001s")
@Schema(description = "耗时", example = "0.001s")
String times;
/**
* 响应状态码
*/
@ApiModelProperty(value = "状态码", example = "200")
@Schema(description = "状态码", example = "200", defaultValue = "200")
Integer status;
/**
* 业务标识码
* 响应消息
*/
@ApiModelProperty(value = "业务标识码", example = "0")
Integer code;
@Schema(description = "消息", example = "OK", defaultValue = "OK")
String message;
/**
* 响应消息
* 业务标识码
*/
@ApiModelProperty(value = "消息", example = "OK")
String message;
@Schema(description = "业务标识码", example = "0", nullable = true)
Integer code;
/**
* 详细信息,通常为参数校验结果或自定义消息
*/
@ApiModelProperty(value = "详细信息", example = "Details message")
@Schema(description = "详细信息", nullable = true)
String details;
/**
* 异常信息,通常在出现服务器错误时会出现该异常
*/
@ApiModelProperty(value = "异常信息", notes = "出现错误时会出现该字段", example = "Error message")
@Schema(description = "异常信息", nullable = true)
String error;
/**
* 响应内容
*/
@ApiModelProperty("内容")
@Schema(description = "内容")
T body;
/**
......@@ -86,8 +95,24 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see com.yiring.common.core.Status
*/
public static <T extends Serializable> Result<T> ok() {
return (Result<T>) Result.builder().status(Status.OK.value()).message(Status.OK.getReasonPhrase()).build();
public static <T> Result<T> ok() {
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 {
* @param body {@link Object}
* @return Result
*/
public static <T extends Serializable> Result<T> ok(T body) {
public static <T> Result<T> ok(T body) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
.message(Status.OK.getReasonPhrase())
.message(t(Status.OK.getReasonPhrase()))
.body(body)
.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
* @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);
}
......@@ -121,7 +166,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @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);
}
......@@ -131,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @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);
}
......@@ -141,13 +186,28 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @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
.builder()
.status(status.value())
.message(status.getReasonPhrase())
.message(t(status.getReasonPhrase()))
.code(code)
.details(details)
.details(t(details))
.build();
if (error != null) {
......@@ -156,4 +216,18 @@ public class Result<T extends Serializable> implements Serializable {
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. */
package com.yiring.common.core;
import com.yiring.common.exception.FailStatusException;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.lang.Nullable;
/**
* API 响应状态码
* 包含系统和业务两个维度
*
* @author ifzm
*/
@SuppressWarnings({ "unused" })
......@@ -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 未实现
*/
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 String reasonPhrase;
Status(int value, String reasonPhrase) {
Status(int value, @PropertyKey(resourceBundle = "i18n.status") String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
......@@ -137,4 +141,29 @@ public enum Status {
public String toString() {
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. */
package com.yiring.common.domain;
import com.yiring.common.snowflake.SnowflakeId;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
......@@ -14,7 +12,7 @@ import org.hibernate.annotations.Comment;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;
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;
@FieldDefaults(level = AccessLevel.PRIVATE)
@SuperBuilder(toBuilder = true)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BasicEntity {
@Comment("主键")
......@@ -50,4 +49,13 @@ public abstract class BasicEntity {
@Column(nullable = false)
@UpdateTimestamp
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 {
* 状态
*/
Status status;
/**
* 异常消息
*/
String message;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
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 java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "IdParam", description = "公共的 ID 查询参数")
@Valid
@Schema(name = "IdParam", description = "公共的 ID 查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -29,7 +27,7 @@ public class IdParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "id", example = "1", required = true)
@NotNull(message = "id 不能为空")
@Parameter(description = "id", example = "1")
@NotBlank
String id;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
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 java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Set;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -20,8 +19,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("IdsParam")
@Valid
@Schema(name = "IdsParam")
@Data
@Builder
@NoArgsConstructor
......@@ -32,15 +30,16 @@ public class IdsParam implements Serializable {
@Serial
private static final long serialVersionUID = -8379896695668632733L;
@ApiModelProperty(value = "ids 多个以逗号分割", example = "1,2", required = true)
@NotEmpty(message = "ids 不能为空")
@Parameter(description = "ids 多个以逗号分割", example = "1,2")
@NotBlank
String ids;
/**
* 获取 String 类型的 ID 集合
*
* @return ID 集合
*/
public Set<String> toIds() {
public Collection<Serializable> toIds() {
return Arrays.stream(this.ids.split(",")).collect(Collectors.toSet());
}
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
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 java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2022/4/27 08:53
*/
@ApiModel(value = "KeywordParam", description = "公共的关键字查询参数")
@Valid
@Schema(name = "KeywordParam", description = "公共的关键字查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -28,6 +27,7 @@ public class KeywordParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456894L;
@ApiModelProperty(value = "关键字", example = "hi")
@Parameter(description = "关键字", example = "hi")
@NotBlank
String keyword;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* 公共的分页排序查询参数
* eg: 支持使用 @Validated(Group.Optional.class) 来忽略校验
*
* @author ifzm
* @version 0.1 2019/3/10 16:29
*/
@ApiModel(value = "PageParam", description = "公共的分页排序查询参数")
@Schema(name = "PageParam", description = "公共的分页排序查询参数")
@Data
@SuperBuilder
@NoArgsConstructor
......@@ -35,20 +36,20 @@ public class PageParam implements Serializable {
@Serial
private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10", required = true)
@NotNull(message = "分页条数不能为空")
@DecimalMin(value = "1", message = "分页条数不能小于1")
@Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull
@Range(min = 1, max = 100)
Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1", required = true)
@NotNull(message = "当前页数不能为空")
@DecimalMin(value = "1", message = "当前页数不能小于1")
@Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull
@Min(1)
Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id")
@Schema(description = "排序字段", defaultValue = "id", example = "id")
String sortField;
@ApiModelProperty(value = "排序方向(ASC|DESC)", example = "DESC")
@Schema(description = "排序方向(ASC|DESC)", defaultValue = "DESC", example = "DESC")
Sort.Direction sortOrder;
/**
......@@ -59,14 +60,27 @@ public class PageParam implements Serializable {
*/
@SuppressWarnings({ "unused" })
public static Pageable toPageable(PageParam param) {
if (param == null) {
if (param == null || Objects.isNull(param.getPageNo()) || Objects.isNull(param.getPageSize())) {
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();
if (Objects.nonNull(param.getSortField())) {
sort = Sort.by(new Sort.Order(param.getSortOrder(), param.getSortField()));
if (Objects.nonNull(sortField)) {
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. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
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 java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "PidParam", description = "公共的父级 ID 查询参数")
@Valid
@Schema(name = "PidParam", description = "公共的父级 ID 查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -29,7 +27,7 @@ public class PidParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "pid", example = "0", required = true)
@NotNull(message = "pid 不能为空")
@Parameter(description = "pid", example = "0")
@NotBlank
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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论