提交 280bfc17 作者: 方治民

合并分支 '3.0' 到 'beta'

3.0

查看合并请求 !14
流水线 #1993 已失败 于阶段
in 34 秒
...@@ -33,7 +33,7 @@ build-job: ...@@ -33,7 +33,7 @@ build-job:
before_script: before_script:
- chmod +x ./gradlew - chmod +x ./gradlew
script: script:
- ./gradlew app:assemble -Dskip-hooks - ./gradlew :app:assemble -Dskip-hooks
artifacts: artifacts:
# 配置构建结果过期时间 # 配置构建结果过期时间
expire_in: 1 day expire_in: 1 day
...@@ -54,12 +54,19 @@ deploy-job: ...@@ -54,12 +54,19 @@ deploy-job:
tags: tags:
- YR-CD - YR-CD
script: script:
# 容器名
- NAME=$CONTAINER_NAME-$CI_BUILD_REF_NAME
# 尝试停止并删除上一个容器
- id=$(docker ps -aqf name=$NAME) && [ "$id" ] && docker stop $id && docker rm -f $id || true
# 尝试删除镜像(先删除再构建,避免产生 <none> image)
- iid=$(docker images -aq $TAG) && [ "$iid" ] && docker rmi -f $iid || true
# 基于 Dockerfile 构建镜像 # 基于 Dockerfile 构建镜像
- docker build -t $TAG . - docker build -t $TAG .
# 尝试删除上一个容器 # 条件判断
- id=$(docker ps -aqf name=$CONTAINER_NAME) && [ "$id" ] && docker rm -f $id || true - PORT=$EXPOSE_PORT
- echo "Branch:" $CI_BUILD_REF_NAME "PORT:" $PORT "Container:" $NAME
# 在本地运行构建好的镜像 # 在本地运行构建好的镜像
- docker run -d --name $CONTAINER_NAME -p $EXPOSE_PORT:8081 $TAG - docker run -d --name $NAME -p $PORT:8081 -e TZ="Asia/Shanghai" $TAG
variables: variables:
# 设置镜像 tag,使用 git tag 标识作为镜像 tag # 设置镜像 tag,使用 git tag 标识作为镜像 tag
TAG: $REGISTRY_REMOTE/basic/$CONTAINER_NAME:$CI_BUILD_REF_NAME TAG: $REGISTRY_REMOTE/basic/$CONTAINER_NAME:$CI_BUILD_REF_NAME
# 指定基础镜像,在其上进行定制 # 指定基础镜像,在其上进行定制
FROM localhost:18500/jdk-17 FROM localhost:18500/jdk-17-ffmpeg
# 维护者信息 # 维护者信息
MAINTAINER ifzm <fangzhimin@yiring.com> MAINTAINER ifzm <fangzhimin@yiring.com>
...@@ -16,6 +16,9 @@ COPY app/build/libs/app-0.0.1-SNAPSHOT.jar app.jar ...@@ -16,6 +16,9 @@ COPY app/build/libs/app-0.0.1-SNAPSHOT.jar app.jar
# RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。 # RUN新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。
# RUN bash -c "touch /app.jar" # RUN bash -c "touch /app.jar"
# 设置时区,解决时区问题
RUN echo "Asia/Shanghai" > /etc/timezone
# 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务 # 声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务
EXPOSE 8081 EXPOSE 8081
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
- [x] 完成项目构建,开发文档编写 - [x] 完成项目构建,开发文档编写
- [x] [conventional-changelog](https://www.cnblogs.com/mengfangui/p/12634845.html) - [x] [conventional-changelog](https://www.cnblogs.com/mengfangui/p/12634845.html)
- [x] 用户及权限模块(目录/菜单/按钮),预览初始化权限配置 [SQL 脚本](./basic-auth/src/main/resources/init-test-mysql.sql) - [x] 用户及权限模块(目录/菜单/按钮),预览初始化权限配置 [SQL 脚本](./basic-auth/src/main/resources/init-test-mysql.sql)
- [x] 通用文件上传模块 - [x] 通用文件上传模块,支持对图片/PDF/MP3/MP4 等文件进行预处理
- [x] WebSocket 模块
- [ ] 通用字典管理模块 - [ ] 通用字典管理模块
- [ ] XXL-JOB 定时任务模块 - [ ] XXL-JOB 定时任务模块
group = 'com.yiring' group = 'com.yiring'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
processResources {
filesMatching('application.yml') {
expand(project.properties)
}
}
bootJar { bootJar {
enabled = true enabled = true
} }
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 💬 Mock/Test Env // 💬 Mock/Test Env
runtimeOnly 'com.h2database:h2' runtimeOnly 'com.h2database:h2'
// 💬 Prod/Dev Env // 💬 Prod/Dev Env
...@@ -22,33 +28,44 @@ dependencies { ...@@ -22,33 +28,44 @@ dependencies {
implementation project(":basic-common:core") implementation project(":basic-common:core")
implementation project(":basic-common:util") implementation project(":basic-common:util")
// Optional: I18n 消息, 包括参数校验、失败的请求提示等
implementation project(":basic-common:i18n")
// Optional: Redis // Optional: Redis
implementation project(":basic-common:redis") implementation project(":basic-common:redis")
// Optional: Doc // Optional: Doc
implementation project(":basic-common:doc") implementation project(":basic-common:doc")
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// Optional: Auth // Optional: Auth
implementation project(":basic-auth") implementation project(":basic-auth")
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}" implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
// Optional: Dict - 数据字典
implementation project(":basic-dict")
// Optional: WebSocket && STOMP 依赖 Auth + Redis 模块
implementation project(":basic-websocket")
// Optional: Minio S3 // Optional: Minio S3
implementation project(":basic-common:minio") implementation project(":basic-common:minio")
// FIX: minio dep // FIX: minio dep
implementation "io.minio:minio:${minioVersion}"
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}" implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
// Optional: 扩展实现在文件上传时对文件进行预处理,依赖 Minio 模块
// Optional: MyBatis Plus // https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "com.baomidou:mybatis-plus-boot-starter:${mybatisPlusVersion}" implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool // hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-extra:${hutoolVersion}" implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types // https://github.com/vladmihalcea/hypersistence-utils
// hibernate-types-55 implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app; package com.yiring.app;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@MapperScan(basePackages = Application.BASE_PACKAGES + ".app.mapper")
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@EntityScan( @EntityScan(
basePackageClasses = { Application.class, Jsr310JpaConverters.class }, basePackageClasses = { Application.class, Jsr310JpaConverters.class },
basePackages = Application.BASE_PACKAGES basePackages = Application.BASE_PACKAGES
) )
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES) @SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES)
public class Application { public class Application {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app.config; package com.yiring.app.config;
import cn.dev33.satoken.exception.NotLoginException; import com.yiring.common.core.I18n;
import com.yiring.app.constant.Code;
import com.yiring.app.exception.CodeException;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.FailStatusException; import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException; import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException; import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/** /**
* 全局错误处理 * 全局错误处理
...@@ -26,83 +19,12 @@ import org.springframework.web.bind.annotation.ResponseBody; ...@@ -26,83 +19,12 @@ import org.springframework.web.bind.annotation.ResponseBody;
* 2017年11月30日 上午11:36:31 * 2017年11月30日 上午11:36:31
*/ */
@Slf4j @Slf4j
@ControllerAdvice @Order
@ResponseBody @RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/** final I18n i18n;
* 参数校验异常
*
* @param e 异常信息
* @return 统一的校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(
value = { BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
)
public Result<String> bindErrorHandler(Exception e) {
String error = "未知参数校验错误";
if (e instanceof ConstraintViolationException) {
error = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
} else {
BindingResult result = null;
if (e instanceof MethodArgumentNotValidException) {
result = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
result = ((BindException) e).getBindingResult();
}
if (result != null) {
error = result.getAllErrors().iterator().next().getDefaultMessage();
}
}
return Result.no(Status.EXPECTATION_FAILED, error);
}
/**
* 不支持的HttpMethod异常
*
* @param e 异常信息
* @return 异常信息反馈 {@link Status#METHOD_NOT_ALLOWED
*/
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public Result<String> httpRequestMethodNotSupportedErrorHandler(Exception e) {
return Result.no(Status.METHOD_NOT_ALLOWED, e.getMessage());
}
/**
* 未登录异常(鉴权失败)
*
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(value = NotLoginException.class)
public Result<String> notLoginErrorHandler() {
return Result.no(Status.UNAUTHORIZED);
}
/**
* 自定义业务异常
*/
@ExceptionHandler(value = CodeException.class)
public Result<String> customCodeExceptionHandler(CodeException e) {
Code code = e.getCode();
return Result.no(Status.BAD_REQUEST, code.value(), code.reason(), null);
}
/**
* 失败状态异常
*/
@ExceptionHandler(value = FailStatusException.class)
public Result<String> failStatusExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus());
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler(value = ClientAbortException.class)
public void clientAbortExceptionHandler() {}
/** /**
* 其他异常 * 其他异常
...@@ -110,9 +32,9 @@ public class GlobalExceptionHandler { ...@@ -110,9 +32,9 @@ public class GlobalExceptionHandler {
* @param e 异常信息 * @param e 异常信息
* @return 统一的500异常信息 {@link Status#INTERNAL_SERVER_ERROR * @return 统一的500异常信息 {@link Status#INTERNAL_SERVER_ERROR
*/ */
@ExceptionHandler(value = Exception.class) @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
public Result<String> defaultErrorHandler(Exception e, HttpServletResponse response) { @ExceptionHandler(Exception.class)
response.setStatus(Status.INTERNAL_SERVER_ERROR.value()); public Result<String> defaultErrorHandler(Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.no(Status.INTERNAL_SERVER_ERROR, e); return Result.no(Status.INTERNAL_SERVER_ERROR, e);
} }
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.constant;
import io.swagger.annotations.ApiModel;
/**
* 业务状态码
* eg: <code>throw new CodeException(Code.FAIL)</code>
*
* @author Jim
* @version 0.1
* 2022/3/25 9:23
*/
@SuppressWarnings({ "unused" })
@ApiModel("业务状态码")
public enum Code {
SUCCESS(0, "成功"),
// TODO: 扩展业务状态
// eg:
// 10001: 用户被禁止登录
FAIL(10000, "测试错误");
private final int value;
private final String reasonPhrase;
Code(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
/**
* Return the integer value of this status code.
*/
public int value() {
return this.value;
}
/**
* Return the reason phrase of this status code.
*/
public String reason() {
return this.reasonPhrase;
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.app.domain; package com.yiring.app.domain.user;
import com.baomidou.mybatisplus.annotation.TableName; import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
/** /**
* 测试表 * 用户扩展
* *
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/4/15 18:34 * 2022/7/13 10:59
*/ */
@Getter @Getter
...@@ -29,21 +28,28 @@ import org.hibernate.annotations.Comment; ...@@ -29,21 +28,28 @@ import org.hibernate.annotations.Comment;
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Entity @Entity
@TableName("TEST_TABLE") @Table(name = "SYS_USER_EXTENSION", uniqueConstraints = @UniqueConstraint(columnNames = "user_id"))
@Table(name = "TEST_TABLE") @Comment("用户扩展表")
@Comment("测试表") public class UserExtension extends BasicEntity implements Serializable {
public class TestTable implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -6168070383092874608L; private static final long serialVersionUID = -1157047754883351972L;
@Comment("主键") @Comment("用户")
@Id @OneToOne
String id; @JoinColumn(nullable = false, name = "user_id")
User user;
@Comment("姓名") @Comment("性别")
String name; Integer gender;
@Comment("年龄") @Comment("年龄")
Integer age; Integer age;
@Comment("简介")
String introduction;
public UserExtension(User user) {
this.user = user;
}
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.domain.user;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2022/7/13 11:11
*/
@Repository
public interface UserExtensionRepository extends JpaRepository<UserExtension, Serializable> {}
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yiring.app.domain.TestTable;
/**
* @author Jim
* @version 0.1
* 2022/4/15 17:25
*/
public interface TestTableMapper extends BaseMapper<TestTable> {}
/**
* @author Jim
* @version 0.1
* 2022/4/5 16:57
*/
package com.yiring.app.mapper;
// MyBatis Mapper/XML 目录
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.service.upload;
import cn.hutool.core.io.FileUtil;
import com.yiring.common.config.EnvConfig;
import com.yiring.common.core.Minio;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.Commons;
import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegFormat;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* @author Jim
* @version 0.1
* 2022/9/23 16:44
*/
@Slf4j
@Primary
@Component
@RequiredArgsConstructor
public class UploadProcessServiceImpl implements UploadProcessService {
final Minio minio;
final EnvConfig env;
@SneakyThrows
@Override
public String handle(String object, MultipartFile file) {
String suffix = FileUtil.getSuffix(object);
// Image: 在文件名上追加图片物理像素
if (isSupportiveImage(suffix)) {
object = handleImage(object, file);
}
// PDF: 在文件名上追加页数,同时在同目录生成 PDF 每一页的图片
if (isPdf(suffix)) {
object = handlePdf(object, file);
}
// Video/Audio: 在文件名上追加时长,视频生成封面图
if (isSupportiveMedia(suffix)) {
object = handleMedia(object, suffix, file);
}
return object;
}
@SneakyThrows
public String handleImage(String object, MultipartFile file) {
@Cleanup
InputStream is = file.getInputStream();
BufferedImage image = ImageIO.read(is);
return fillSuffix(object, image.getWidth() + "x" + image.getHeight());
}
@SneakyThrows
public String handlePdf(String object, MultipartFile file) {
@Cleanup
InputStream is = file.getInputStream();
@Cleanup
PDDocument doc = PDDocument.load(is);
int pages = doc.getNumberOfPages();
// 构建具有 PDF 页数标记的存储地址
String filepath = fillSuffix(object, "P" + pages);
// 将 PDF 解析成图片上传
String format = "jpg";
PDFRenderer renderer = new PDFRenderer(doc);
for (int i = 0; i < pages; i++) {
@Cleanup
ByteArrayOutputStream os = new ByteArrayOutputStream();
BufferedImage image = renderer.renderImageWithDPI(i, 144);
ImageIO.write(image, format, os);
@Cleanup
InputStream io = new ByteArrayInputStream(os.toByteArray());
minio.putObject(io, MediaType.IMAGE_JPEG_VALUE, filepath + "." + (i + 1) + "." + format);
}
return filepath;
}
@SneakyThrows
public String handleMedia(String object, String suffix, MultipartFile file) {
// 判断是否配置 ffmpeg 环境
FFmpeg ffmpeg = new FFmpeg();
FFprobe ffprobe = new FFprobe();
try {
if (!ffmpeg.isFFmpeg() || !ffprobe.isFFprobe()) {
return object;
}
} catch (Exception e) {
log.warn(e.getMessage());
return object;
}
// 将上传的文件转存一份到本地临时目录
Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename());
FileUtil.mkParentDirs(tempFile);
file.transferTo(tempFile);
// 解析媒体文件
FFmpegProbeResult probeResult = ffprobe.probe(tempFile.toString());
FFmpegFormat format = probeResult.getFormat();
// 构建具有时长(秒)标记的存储地址
long sec = (long) format.duration;
String filepath = fillSuffix(object, "T" + sec);
// 视频截取首帧可见画面作为封面
if (isSupportiveVideo(suffix)) {
// 大视频文件切片上传(> 5s)
if (sec > 5) {
filepath = handleVideoToHls(filepath, tempFile, ffmpeg, ffprobe);
}
// 使用 ffmpeg 截取视频首帧图片
Path path = Paths.get(tempFile.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
handleVideoScreenshot(tempFile.toString(), path.toString(), filepath, ffmpeg, ffprobe);
}
// 删除为本次上传进行本地处理所创建的整个临时文件夹目录
FileUtil.del(tempFile.getParent());
return filepath;
}
@SneakyThrows
public void handleVideoScreenshot(String in, String out, String object, FFmpeg ffmpeg, FFprobe ffprobe) {
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(in)
.overrideOutputFiles(true)
.addOutput(out)
.setFrames(1)
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
// 判断封面图是否生成
Path path = Paths.get(out);
if (Files.exists(path)) {
// 上传封面图
minio.putObject(path.toFile(), getObjectFolder(object));
}
}
@SneakyThrows
public String handleVideoToHls(String object, Path path, FFmpeg ffmpeg, FFprobe ffprobe) {
String originName = FileUtil.getName(object);
String objectFolder = object.replace("/" + originName, "");
String targetName = originName.replaceAll("^(.*)\\." + FileUtil.getSuffix(originName) + "$", "$1.m3u8");
// 使用 ffmpeg 将视频文件转换成 hls 切片文件 m3u8+ts
// "-vsync", "2", "-c:v", "copy", "-c:a", "copy", "-tune", "fastdecode", "-hls_wrap", "0", "-hls_time", "10", "-hls_list_size", "0", "-threads", "12"
final FFmpegProbeResult probe = ffprobe.probe(path.toString());
Path tempFile = Paths.get(path.getParent().toString(), targetName);
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(path.toString())
.overrideOutputFiles(true)
.addOutput(tempFile.toString())
.setFormat(probe.getFormat().format_name)
.setStrict(FFmpegBuilder.Strict.STRICT)
.setFormat("hls")
.setPreset("ultrafast")
.addExtraArgs(
"-vsync",
"2",
"-c:v",
"copy",
"-c:a",
"copy",
"-tune",
"fastdecode",
"-hls_wrap",
"0",
"-hls_time",
"5",
"-hls_list_size",
"0",
"-hls_segment_filename",
tempFile.toString().replaceAll("^(.*)\\.m3u8$", "$1-%d.ts"),
"-threads",
"8"
)
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
if (Files.exists(tempFile)) {
Pattern pattern = Pattern.compile("^.*\\.ts$");
// 解析 m3u8 文件
List<String> lines = FileUtil.readLines(tempFile.toString(), StandardCharsets.UTF_8);
// 获取 ts 切片文件
List<String> tss = lines.stream().filter(line -> pattern.matcher(line).matches()).toList();
// 上传 ts 切片文件
for (String ts : tss) {
Path temp = Paths.get(tempFile.getParent().toString(), ts);
minio.putObject(temp.toFile(), objectFolder);
}
// 上传 m3u8 索引文件
ObjectWriteResponse objectWriteResponse = minio.putObject(tempFile.toFile(), objectFolder);
object = objectWriteResponse.object();
// 手动 GC 一次
System.gc();
}
return object;
}
public static boolean isSupportiveMedia(String suffix) {
return isSupportiveVideo(suffix) || isSupportiveAudio(suffix);
}
public static boolean isSupportiveVideo(String suffix) {
return List.of("mp4", "flv", "avi", "rmvb", "rm", "wmv", "mkv", "mpg", "mpeg").contains(suffix.toLowerCase());
}
public static boolean isSupportiveAudio(String suffix) {
return List.of("mp3", "wav").contains(suffix.toLowerCase());
}
public static boolean isSupportiveImage(String suffix) {
return List.of("png", "jpg", "webp", "gif", "tif", "svg", "bmp").contains(suffix.toLowerCase());
}
public static boolean isPdf(String suffix) {
return "pdf".equalsIgnoreCase(suffix);
}
public static String fillSuffix(String object, String fill) {
String suffix = FileUtil.getSuffix(object);
String regex = "^(.*)\\." + suffix + "$";
return object.replaceAll(regex, "$1." + fill + "." + suffix);
}
public static String getObjectFolder(String object) {
String sourceName = FileUtil.getName(object);
return object.replace("/" + sourceName, "");
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.app.vo.user;
import com.yiring.common.jackson.MappingSerialize;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
/**
* @author Jim
* @version 0.1
* 2022/7/13 11:36
*/
@Schema(name = "UserExtensionVo", description = "用户扩展信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserExtensionVo implements Serializable {
@Serial
private static final long serialVersionUID = -2251567849918281906L;
@Schema(description = "性别", example = "男", allowableValues = { "男", "女" }, type = "string")
@MappingSerialize(mapping = "0:女,1:男")
Integer gender;
@Schema(description = "年龄", example = "18")
Integer age;
@Schema(description = "简介", example = "Hi")
String introduction;
}
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.app.web.example; package com.yiring.app.web.example;
import cn.hutool.extra.spring.SpringUtil; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckSafe;
import cn.dev33.satoken.annotation.SaIgnore;
import com.github.xiaoymin.knife4j.annotations.ApiSupport; import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.app.constant.Code; import com.yiring.app.domain.user.UserExtension;
import com.yiring.app.domain.TestTable; import com.yiring.app.domain.user.UserExtensionRepository;
import com.yiring.app.exception.CodeException; import com.yiring.app.vo.user.UserExtensionVo;
import com.yiring.app.mapper.TestTableMapper; import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.service.FileManageService;
import com.yiring.common.util.Commons;
import com.yiring.common.util.FileUtils; import com.yiring.common.util.FileUtils;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletResponse; import java.util.Optional;
import javax.validation.Valid; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.Example;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 示例接口 * 示例接口
*
* @author Jim * @author Jim
*/ */
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = 0) @ApiSupport(order = 0)
@Api(tags = "示例", description = "Example") @Tag(name = "示例", description = "Example")
@RequestMapping("/example/") @RequestMapping("/example/")
@RestController @RestController
@RequiredArgsConstructor
public class ExampleController { public class ExampleController {
String text = "😎 Hello World"; final I18n i18n;
final Auths auths;
final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
@Operation(summary = "Hello World")
@GetMapping @GetMapping
public Result<String> hello() { public Result<String> hello() {
return Result.ok(text); return Result.ok("example.hello");
} }
/** /**
* 测试失败自定义状态信息输出 * 测试失败自定义状态信息输出
*/ */
@Operation(summary = "测试失败")
@GetMapping("fail") @GetMapping("fail")
public Result<String> fail() { public Result<String> fail() {
throw new CodeException(Code.FAIL); throw BusinessException.i18n("Code.1");
} }
@SaCheckLogin
@Operation(summary = "分页条件查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<String>> page(@Valid PageParam pageParam) { public Result<PageVo<String>> page(@ParameterObject @Validated PageParam param) {
log.info("PageParam: {}", pageParam); log.debug("PageParam: {}", param);
String text = i18n.get("example.hello");
List<String> data = Arrays.asList(text.split(" ")); List<String> data = Arrays.asList(text.split(" "));
PageVo<String> vo = PageVo.build(data, data.size()); PageVo<String> vo = PageVo.build(data, data.size());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @Operation(summary = "JSON 传参")
@GetMapping("download") @PostMapping("json")
public void download(HttpServletResponse response) throws IOException { public Result<PageVo<String>> json(
@RequestBody(required = false) @Validated(Group.Optional.class) PageParam param
) {
return page(param);
}
@SaIgnore
@DownloadResponse
@SneakyThrows(IOException.class)
@Operation(summary = "文件下载")
@GetMapping(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void download(HttpServletResponse response) {
ClassPathResource resource = new ClassPathResource("static/cat.jpg"); ClassPathResource resource = new ClassPathResource("static/cat.jpg");
FileUtils.download(response, resource.getFile()); FileUtils.download(response, resource.getFile());
} }
@ApiOperation("测试 MyBatis Plus 查询") @Operation(summary = "文件上传")
@GetMapping("test") @PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> test() { public Result<String> upload(
List<TestTable> tests = SpringUtil.getBean(TestTableMapper.class).selectList(null); @Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file,
return Result.ok(tests.stream().map(TestTable::getName).reduce((a, b) -> a + "," + b).orElse("")); @ParameterObject @Validated IdParam param
) {
log.info("upload params: {}", param);
try {
String link = fileManageService.upload(file);
return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
}
}
@SaCheckSafe
@SaCheckLogin
@Operation(summary = "查询用户属性")
@GetMapping("findUserExtensionInfo")
public Result<UserExtensionVo> findUserExtensionInfo() {
User user = auths.getLoginUser();
Optional<UserExtension> optional = userExtensionRepository.findOne(Example.of(new UserExtension(user)));
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
UserExtension ext = optional.get();
UserExtensionVo vo = Commons.transform(ext, UserExtensionVo.class);
return Result.ok(vo);
} }
} }
# 环境变量 # 环境变量
env: env:
host: 192.168.0.156 host: 192.168.0.156
prod: false
extra:
username: admin
password: 123456
# ----------------------------------------------
spring: spring:
datasource: datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai url: jdbc:postgresql://${env.host}:5432/basic_app
username: root username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true open-in-view: true
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: false show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
redis: # https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
database: 5 schema_update:
host: ${env.host} unique_constraint_strategy: RECREATE_QUIETLY
port: 6379 # https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827
# Optional: MyBatis Plus jakarta:
mybatis-plus: persistence:
global-config: sharedCache:
banner: false mode: UNSPECIFIED
data:
redis:
database: 5
port: 6379
host: ${env.host}
password: ${env.extra.password}
# ----------------------------------------------
# ----------------------------------------------
# Spring Doc
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: false
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: false language: zh_cn
enableOpenApi: true
enableDebug: true enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio # minio
minio: minio:
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: fatal org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# ----------------------------------------------
...@@ -8,8 +8,16 @@ spring: ...@@ -8,8 +8,16 @@ spring:
hibernate: hibernate:
# 关闭 hibernate-types banner 日志信息 # 关闭 hibernate-types banner 日志信息
types.print.banner: false types.print.banner: false
data:
redis:
repositories:
enabled: false
logging: logging:
level: level:
# 关闭接口扫描 CachingOperationNameGenerator 日志 # 关闭接口扫描 CachingOperationNameGenerator 日志
springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator: WARN springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator: WARN
# 关闭接口扫描 ApiListingReferenceScanner 日志
springfox.documentation.spring.web.scanners.ApiListingReferenceScanner: WARN
# https://github.com/spring-projects/spring-framework/issues/29612
org.springframework.core.LocalVariableTableParameterNameDiscoverer: ERROR
# 环境变量 # 环境变量
env: env:
host: 127.0.0.1 host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring: spring:
datasource: datasource:
url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai url: jdbc:mysql://${env.host}:3306/basic_app?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect database-platform: org.hibernate.dialect.MySQL8Dialect
open-in-view: true open-in-view: true
...@@ -20,33 +24,30 @@ spring: ...@@ -20,33 +24,30 @@ spring:
database: 5 database: 5
host: ${env.host} host: ${env.host}
port: 6379 port: 6379
password: 123456 password: ${env.extra.password}
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: false enableOpenApi: false
enableDebug: true enableDebug: true
# minio # minio
minio: minio:
access-key: minioadmin access-key: ${env.extra.username}
secret-key: minioadmin secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# 环境变量 # 环境变量
env: env:
host: 192.168.0.156 # host: 192.168.0.156
host: 127.0.0.1
prod: false
extra:
# username: admin
password: 123456
username: postgres
ffmpeg:
path: D:\Environments\FFmpeg\bin
# ----------------------------------------------
spring: spring:
datasource: datasource:
url: jdbc:postgresql://${env.host}:5432/basic_app url: jdbc:postgresql://${env.host}:5432/basic_app
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
jpa: jpa:
database-platform: org.hibernate.dialect.PostgreSQL10Dialect database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true open-in-view: true
hibernate: hibernate:
ddl-auto: update ddl-auto: update
...@@ -16,37 +25,60 @@ spring: ...@@ -16,37 +25,60 @@ spring:
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
redis: # https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
database: 5 schema_update:
host: ${env.host} unique_constraint_strategy: RECREATE_QUIETLY
port: 6379 # https://github.com/spring-projects/spring-data-jpa/issues/2717
password: 123456 # https://hibernate.atlassian.net/browse/HHH-15827
jakarta:
# Optional: MyBatis Plus persistence:
mybatis-plus: sharedCache:
global-config: mode: UNSPECIFIED
banner: false data:
redis:
database: 5
port: 6379
host: ${env.host}
password: ${env.extra.password}
# ----------------------------------------------
# ----------------------------------------------
# Spring Doc
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: false
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
language: zh_cn
enableOpenApi: true enableOpenApi: true
enableDebug: true enableDebug: true
# ----------------------------------------------
# ----------------------------------------------
# minio # minio
minio: minio:
access-key: minioadmin access-key: minioadmin
secret-key: minioadmin secret-key: minioadmin
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
# ----------------------------------------------
# ----------------------------------------------
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
# ----------------------------------------------
# 环境变量 # 环境变量
env: env:
host: 127.0.0.1 host: 127.0.0.1
prod: false
extra:
username: admin
password: Hd)XZgtCa&NG~oe@
spring: spring:
datasource: datasource:
...@@ -20,34 +24,30 @@ spring: ...@@ -20,34 +24,30 @@ spring:
database: 5 database: 5
host: ${env.host} host: ${env.host}
port: 6379 port: 6379
password: 123456 password: ${env.extra.password}
# Optional: MyBatis Plus
mybatis-plus:
global-config:
banner: false
# knife4j # knife4j
knife4j: knife4j:
enable: true enable: true
basic: basic:
enable: false enable: false
username: admin username: ${env.extra.username}
password: 123456 password: ${env.extra.password}
setting: setting:
enableOpenApi: true enableOpenApi: true
enableDebug: true enableDebug: true
# minio # minio
minio: minio:
access-key: minioadmin access-key: ${env.extra.username}
secret-key: minioadmin secret-key: ${env.extra.password}
end-point: "http://${env.host}:18100" end-point: "http://${env.host}:18100"
bucket: basic bucket: public
domain: ${minio.endpoint}/${minio.bucket} domain: ${minio.end-point}
logging: logging:
level: level:
# sql bind parameter # sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace org.hibernate.type.descriptor.sql.BasicBinder: trace
# request log
com.yiring.common.aspect.RequestAspect: info
app:
version: ${version}
server: server:
port: 8081 port: 8081
servlet: servlet:
context-path: /api context-path: /api
tomcat:
max-http-form-post-size: 20MB
spring: spring:
servlet:
# 文件上传大小限制
multipart:
max-file-size: 10MB
max-request-size: 30MB
application: application:
name: "basic-api-app" name: "basic-api-app"
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1048MB
profiles: profiles:
include: auth, conf-patch include: auth, conf-patch
active: dev-postgresql active: dev-postgresql
......
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
jakarta.validation.constraints.AssertFalse.message=\u53EA\u80FD\u4E3Afalse
jakarta.validation.constraints.AssertTrue.message=\u53EA\u80FD\u4E3Atrue
jakarta.validation.constraints.DecimalMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
jakarta.validation.constraints.DecimalMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
jakarta.validation.constraints.Digits.message=\u6570\u5B57\u7684\u503C\u8D85\u51FA\u4E86\u5141\u8BB8\u8303\u56F4(\u53EA\u5141\u8BB8\u5728{integer}\u4F4D\u6574\u6570\u548C{fraction}\u4F4D\u5C0F\u6570\u8303\u56F4\u5185)
jakarta.validation.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
jakarta.validation.constraints.Future.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4
jakarta.validation.constraints.FutureOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u6216\u73B0\u5728\u7684\u65F6\u95F4
jakarta.validation.constraints.Max.message=\u6700\u5927\u4E0D\u80FD\u8D85\u8FC7{value}
jakarta.validation.constraints.Min.message=\u6700\u5C0F\u4E0D\u80FD\u5C0F\u4E8E{value}
jakarta.validation.constraints.Negative.message=\u5FC5\u987B\u662F\u8D1F\u6570
jakarta.validation.constraints.NegativeOrZero.message=\u5FC5\u987B\u662F\u8D1F\u6570\u6216\u96F6
jakarta.validation.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
jakarta.validation.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
jakarta.validation.constraints.NotNull.message=\u4E0D\u80FD\u4E3Anull
jakarta.validation.constraints.Null.message=\u5FC5\u987B\u4E3Anull
jakarta.validation.constraints.Past.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65F6\u95F4
jakarta.validation.constraints.PastOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u6216\u73B0\u5728\u7684\u65F6\u95F4
jakarta.validation.constraints.Pattern.message=\u9700\u8981\u5339\u914D\u6B63\u5219\u8868\u8FBE\u5F0F"{regexp}"
jakarta.validation.constraints.Positive.message=\u5FC5\u987B\u662F\u6B63\u6570
jakarta.validation.constraints.PositiveOrZero.message=\u5FC5\u987B\u662F\u6B63\u6570\u6216\u96F6
jakarta.validation.constraints.Size.message=\u4E2A\u6570\u5FC5\u987B\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.CreditCardNumber.message=\u4E0D\u5408\u6CD5\u7684\u4FE1\u7528\u5361\u53F7\u7801
org.hibernate.validator.constraints.Currency.message=\u4E0D\u5408\u6CD5\u7684\u8D27\u5E01 (\u5FC5\u987B\u662F{value}\u5176\u4E2D\u4E4B\u4E00)
org.hibernate.validator.constraints.EAN.message=\u4E0D\u5408\u6CD5\u7684{type}\u6761\u5F62\u7801
org.hibernate.validator.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
org.hibernate.validator.constraints.Length.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.CodePointLength.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.LuhnCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, Luhn\u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod10Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod11Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2111\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.ModCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, {modType}\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.ParametersScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.Range.message=\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.ScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.URL.message=\u9700\u8981\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL
org.hibernate.validator.constraints.time.DurationMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
org.hibernate.validator.constraints.time.DurationMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
/* (C) 2022 YiRing, Inc. */
package com.yiring;
import com.yiring.app.domain.TestTable;
import com.yiring.app.mapper.TestTableMapper;
import java.util.List;
import javax.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;
/**
* @author Jim
* @version 0.1
* 2022/4/15 17:25
*/
@SpringBootTest
public class MapperSampleTest {
@Resource
TestTableMapper testTableMapper;
@Test
public void testSelect() {
List<TestTable> tests = testTableMapper.selectList(null);
Assert.isTrue(tests.size() > 0, "查询失败");
}
}
...@@ -9,20 +9,25 @@ dependencies { ...@@ -9,20 +9,25 @@ dependencies {
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar']) implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// sa-token // sa-token
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}" implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
implementation "cn.dev33:sa-token-dao-redis-jackson:${saTokenVersion}" implementation "cn.dev33:sa-token-dao-redis-jackson:${saTokenVersion}"
// Sa-Token 整合 Redis (使用 jackson 序列化方式)
implementation 'org.apache.commons:commons-pool2'
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// hutool-core // hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types // https://github.com/vladmihalcea/hypersistence-utils
// hibernate-types-55 // hypersistence-utils-hibernate-60
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}" implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
} }
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.config;
import cn.dev33.satoken.exception.*;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 授权异常拦截处理
*
* @author Jim
* @version 0.1
* 2023/1/12 14:06
*/
@Slf4j
@Order(1)
@RestControllerAdvice
@RequiredArgsConstructor
public class AuthExceptionHandler {
/**
* 未登录异常(鉴权失败)
*
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(NotLoginException.class)
public Result<String> unauthorizedErrorHandler() {
return Result.no(Status.UNAUTHORIZED);
}
/**
* 1. 二级认证失败异常
* 2. 角色条件不满足
* 3. 权限条件不满足
* 4. HTTP Basic 验证不通过
* 5. 用户被禁止访问该服务
* 6. API 被禁用
*
* @return 异常信息反馈 {@link Status#FORBIDDEN
*/
@ExceptionHandler(
{
// https://sa-token.dev33.cn/doc.html#/up/safe-auth
NotSafeException.class,
// https://sa-token.dev33.cn/doc.html#/use/at-check
NotRoleException.class,
NotPermissionException.class,
// https://sa-token.dev33.cn/doc.html#/up/basic-auth
NotBasicAuthException.class,
// https://sa-token.dev33.cn/doc.html#/up/disable
DisableServiceException.class,
ApiDisabledException.class,
}
)
public Result<String> forbiddenHandler(Exception e) {
if (e instanceof NotSafeException) {
return Result.no(Status.FORBIDDEN, "Code.10000");
}
if (e instanceof NotRoleException) {
return Result.no(Status.FORBIDDEN, "Code.10001");
}
if (e instanceof NotPermissionException) {
return Result.no(Status.FORBIDDEN, "Code.10002");
}
if (e instanceof NotBasicAuthException) {
return Result.no(Status.FORBIDDEN, "Code.10003");
}
log.warn(e.getMessage(), e);
return Result.no(Status.FORBIDDEN);
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.config; package com.yiring.auth.config;
import cn.dev33.satoken.interceptor.SaRouteInterceptor; import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod; import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.util.Auths;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
...@@ -22,24 +23,24 @@ public class SaTokenConfigure implements WebMvcConfigurer { ...@@ -22,24 +23,24 @@ public class SaTokenConfigure implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的路由拦截器 // 注册 Sa-Token 路由拦截器
// 可搭配使用注解实现鉴权: https://sa-token.dev33.cn/doc.html#/use/at-check
registry registry
.addInterceptor( .addInterceptor(
new SaRouteInterceptor((req, res, handler) -> { new SaInterceptor(handle -> {
// 登录认证 -- 拦截所有路由,并排除 /auth/** 用于开放授权相关, 以及 swagger 相关 // 登录认证 -- 拦截所有路由,并排除 /auth/** 用于开放授权相关, 以及 swagger 相关
SaRouter SaRouter
.match("/**") .match("/**")
.notMatchMethod(SaHttpMethod.OPTIONS.name()) .notMatchMethod(SaHttpMethod.OPTIONS.name())
// 实现用户权限相关后应移除下行代码
// TODO
// .notMatch("/**") // .notMatch("/**")
// 示例接口
.notMatch("/example/**") .notMatch("/example/**")
// 授权相关接口(登录、登出、注册等)
.notMatch("/auth/**") .notMatch("/auth/**")
.notMatch("/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css") .notMatch("/favicon.ico", "/**/*.html", "/**/*.js", "/**/*.css")
.notMatch("/v2/api-docs/**", "/v3/api-docs/**", "/swagger-resources/**") .notMatch("/v2/api-docs/**", "/v3/api-docs/**", "/swagger-resources/**")
.check(r -> StpUtil.checkLogin()); .check(r -> StpUtil.checkLogin());
// 管理员权限才可访问的路由地址
SaRouter.match("/sys/**", r -> StpUtil.checkRoleOr(Auths.ADMIN_ROLES.toArray(new String[0])));
}) })
) )
.addPathPatterns("/**"); .addPathPatterns("/**");
......
...@@ -6,13 +6,15 @@ import com.yiring.auth.domain.permission.Permission; ...@@ -6,13 +6,15 @@ import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 获取登录用户权限信息实现 * 获取登录用户权限信息实现
...@@ -22,11 +24,12 @@ import org.springframework.stereotype.Component; ...@@ -22,11 +24,12 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37 * 2022/3/25 9:37
*/ */
@Transactional(readOnly = true)
@Component @Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface { public class StpInterfaceImpl implements StpInterface {
@Resource final UserRepository userRepository;
UserRepository userRepository;
@Override @Override
public List<String> getPermissionList(Object loginId, String loginType) { public List<String> getPermissionList(Object loginId, String loginType) {
...@@ -36,6 +39,7 @@ public class StpInterfaceImpl implements StpInterface { ...@@ -36,6 +39,7 @@ public class StpInterfaceImpl implements StpInterface {
.stream() .stream()
.map(Role::getPermissions) .map(Role::getPermissions)
.flatMap(Set::stream) .flatMap(Set::stream)
.distinct()
.map(Permission::getUid) .map(Permission::getUid)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
...@@ -48,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface { ...@@ -48,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface {
/** /**
* 根据 id 获取用户信息 * 根据 id 获取用户信息
*
* @param loginId 登录 ID * @param loginId 登录 ID
* @return 用户信息 * @return 用户信息
*/ */
private User getUser(Object loginId) { public User getUser(Object loginId) {
String id = Objects.toString(loginId); String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id); Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) { if (optional.isEmpty()) {
throw new RuntimeException("用户不存在"); throw Status.NOT_FOUND.exception("Code.1000");
} }
return optional.get(); return optional.get();
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.permission; package com.yiring.auth.domain.permission;
import com.alibaba.fastjson.JSONObject; import static com.yiring.auth.domain.permission.Permission.DELETE_SQL;
import com.vladmihalcea.hibernate.type.json.JsonType; import static com.yiring.auth.domain.permission.Permission.TABLE_NAME;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/** /**
* 权限 * 权限
...@@ -29,19 +34,30 @@ import org.hibernate.annotations.TypeDef; ...@@ -29,19 +34,30 @@ import org.hibernate.annotations.TypeDef;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity @Entity
@TypeDef(name = "json", typeClass = JsonType.class)
@Table( @Table(
name = "SYS_PERMISSION", name = TABLE_NAME,
indexes = { @Index(columnList = "type"), @Index(columnList = "pid"), @Index(columnList = "tree") } indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Permission.Fields.type),
@Index(columnList = Permission.Fields.pid),
@Index(columnList = Permission.Fields.tree),
}
) )
@Comment("系统权限") @Comment("系统权限")
public class Permission extends BasicEntity implements Serializable { public class Permission extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_PERMISSION";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = -2001221843529000953L; private static final long serialVersionUID = -2001221843529000953L;
@Comment("类型(MENU: 菜单, BUTTON: 按钮)") @Comment("类型(MENU: 菜单, BUTTON: 按钮)")
@Column(nullable = false)
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
Type type; Type type;
...@@ -49,10 +65,11 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -49,10 +65,11 @@ public class Permission extends BasicEntity implements Serializable {
Integer serial; Integer serial;
@Comment("标识") @Comment("标识")
@Column(unique = true, nullable = false) @Column(nullable = false)
String uid; String uid;
@Comment("名称") @Comment("名称")
@Column(nullable = false)
String name; String name;
@Comment("路径") @Comment("路径")
...@@ -83,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -83,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable {
* 可用于扩展一些前端可能用到的路由参数 * 可用于扩展一些前端可能用到的路由参数
*/ */
@Comment("扩展元数据信息") @Comment("扩展元数据信息")
@org.hibernate.annotations.Type(type = "json") @org.hibernate.annotations.Type(JsonType.class)
@Column(columnDefinition = "json") @Column(columnDefinition = "json")
JSONObject meta; JSONObject meta;
...@@ -107,10 +124,11 @@ public class Permission extends BasicEntity implements Serializable { ...@@ -107,10 +124,11 @@ public class Permission extends BasicEntity implements Serializable {
/** /**
* 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整 * 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整
*
* @return JSON 格式 Meta 元数据 * @return JSON 格式 Meta 元数据
*/ */
public JSONObject getMetaJson() { public JSONObject getMetaJson() {
JSONObject meta = new JSONObject(true); JSONObject meta = new JSONObject();
meta.put("title", this.name); meta.put("title", this.name);
meta.put("icon", this.icon); meta.put("icon", this.icon);
meta.put("orderNo", this.serial); meta.put("orderNo", this.serial);
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.role; package com.yiring.auth.domain.role;
import static com.yiring.auth.domain.role.Role.DELETE_SQL;
import static com.yiring.auth.domain.role.Role.TABLE_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/** /**
* 角色 * 角色
...@@ -31,11 +37,21 @@ import org.hibernate.annotations.Comment; ...@@ -31,11 +37,21 @@ import org.hibernate.annotations.Comment;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity @Entity
@Table(
name = TABLE_NAME,
indexes = @Index(columnList = BasicEntity.Fields.deleteTime),
uniqueConstraints = { @UniqueConstraint(columnNames = { Role.Fields.uid, BasicEntity.Fields.deleteTime }) }
)
@Comment("系统角色") @Comment("系统角色")
@Table(name = "SYS_ROLE", indexes = { @Index(columnList = "uid", unique = true) })
public class Role extends BasicEntity implements Serializable { public class Role extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_ROLE";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = 910404402503275957L; private static final long serialVersionUID = 910404402503275957L;
...@@ -44,8 +60,12 @@ public class Role extends BasicEntity implements Serializable { ...@@ -44,8 +60,12 @@ public class Role extends BasicEntity implements Serializable {
String uid; String uid;
@Comment("名称") @Comment("名称")
@Column(nullable = false)
String name; String name;
@Comment("是否启用")
Boolean enable;
@JsonIgnore @JsonIgnore
@Builder.Default @Builder.Default
@Comment("权限集合") @Comment("权限集合")
......
...@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role; ...@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role;
import java.io.Serializable; import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/** /**
* 角色接口 * 角色接口
...@@ -10,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaRepository; ...@@ -10,4 +11,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
* @author ifzm * @author ifzm
* 2018/9/4 8:49 * 2018/9/4 8:49
*/ */
@Repository
public interface RoleRepository extends JpaRepository<Role, Serializable> {} public interface RoleRepository extends JpaRepository<Role, Serializable> {}
...@@ -4,17 +4,19 @@ package com.yiring.auth.domain.user; ...@@ -4,17 +4,19 @@ package com.yiring.auth.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import javax.persistence.*;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment; import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where; import org.hibernate.annotations.Where;
/** /**
...@@ -32,45 +34,49 @@ import org.hibernate.annotations.Where; ...@@ -32,45 +34,49 @@ import org.hibernate.annotations.Where;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Where(clause = "deleted = false") @SQLDelete(sql = User.DELETE_SQL)
@SQLDeleteAll(sql = User.DELETE_SQL)
@Where(clause = User.Where.EXIST)
@Entity @Entity
@Table(name = "SYS_USER", indexes = { @Index(columnList = "enabled"), @Index(columnList = "deleted") }) @Table(
name = User.TABLE_NAME,
indexes = { @Index(columnList = User.Fields.enabled), @Index(columnList = BasicEntity.Fields.deleteTime) },
uniqueConstraints = {
@UniqueConstraint(columnNames = { User.Fields.username, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.mobile, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.email, BasicEntity.Fields.deleteTime }),
}
)
@Comment("系统用户") @Comment("系统用户")
public class User extends BasicEntity implements Serializable { public class User extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_USER";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial @Serial
private static final long serialVersionUID = -5787847701210907511L; private static final long serialVersionUID = -5787847701210907511L;
@Comment("头像")
String avatar;
@Comment("真实姓名") @Comment("真实姓名")
String realName; String realName;
@Comment("用户名") @Comment("用户名")
@Column(unique = true)
String username; String username;
@Comment("手机号") @Comment("手机号")
@Column(unique = true)
String mobile; String mobile;
@Comment("邮箱") @Comment("邮箱")
@Column(unique = true)
String email; String email;
@Comment("密码") @Comment("密码")
String password; String password;
@Comment("简介")
String introduction;
@Comment("头像")
String avatar;
@Comment("是否启用") @Comment("是否启用")
Boolean enabled; Boolean enabled;
@Comment("是否删除")
Boolean deleted;
@JsonIgnore @JsonIgnore
@Builder.Default @Builder.Default
@Comment("角色集合") @Comment("角色集合")
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("LoginParam") @Schema(name = "LoginParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,11 +27,11 @@ public class LoginParam implements Serializable { ...@@ -27,11 +27,11 @@ public class LoginParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456895L; private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "账号(支持用户名/手机号/邮箱)", example = "admin", required = true) @Parameter(description = "账号(支持用户名/手机号/邮箱)", example = "admin")
@NotEmpty(message = "账号不能为空") @NotEmpty(message = "账号不能为空")
String account; String account;
@ApiModelProperty(value = "密码", example = "123456", required = true) @Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空") @NotEmpty(message = "密码不能为空")
String password; String password;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("RegisterParam") @Schema(name = "RegisterParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,32 +28,29 @@ public class RegisterParam implements Serializable { ...@@ -28,32 +28,29 @@ public class RegisterParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456895L; private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "用户名", example = "admin", required = true) @Parameter(description = "用户名", example = "admin")
@NotEmpty(message = "用户名不能为空") @NotEmpty(message = "用户名不能为空")
String username; String username;
@ApiModelProperty(value = "密码", example = "123456", required = true) @Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空") @NotEmpty(message = "密码不能为空")
String password; String password;
@ApiModelProperty(value = "真实姓名", example = "管理员", required = true) @Parameter(description = "真实姓名", example = "管理员")
@NotEmpty(message = "真实姓名不能为空") @NotEmpty(message = "真实姓名不能为空")
String realName; String realName;
@ApiModelProperty(value = "手机号", example = "13012345678", required = true) @Parameter(description = "手机号", example = "13012345678")
@NotEmpty(message = "手机号不能为空") @NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确") @Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确")
String mobile; String mobile;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Parameter(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com") @Parameter(description = "邮箱", example = "developer@yiring.com")
String email; String email;
@ApiModelProperty(value = "简介", example = "平台管理员") @Parameter(description = "是否启用", example = "true")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
Boolean enable; Boolean enable;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 公共的可选父级 ID 查询参数 * 安全校验参数
* *
* @author ifzm * @author ifzm
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "OptionalPidParam", description = "公共的可选父级 ID 查询参数") @Schema(name = "SafeParam")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class OptionalPidParam implements Serializable { public class SafeParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = 9106494470582579138L;
@ApiModelProperty(value = "pid", example = "0") @Parameter(description = "密码", example = "123456")
String pid; @NotEmpty(message = "密码不能为空")
String password;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.permission; package com.yiring.auth.param.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel; import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 权限信息入参类 * 权限信息入参类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionParam") @Schema(name = "PermissionParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -31,43 +34,47 @@ public class PermissionParam implements Serializable { ...@@ -31,43 +34,47 @@ public class PermissionParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -6781934969837655538L; private static final long serialVersionUID = -6781934969837655538L;
@ApiModelProperty(value = "权限类型", example = "MENU", required = true) @Schema(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Schema(description = "权限类型", example = "MENU")
@NotNull(message = "权限类型不能为空") @NotNull(message = "权限类型不能为空")
Permission.Type type; Permission.Type type;
@ApiModelProperty(value = "序号", example = "1") @Schema(description = "序号", example = "1")
Integer serial; Integer serial;
@ApiModelProperty(value = "标识", example = "Dashboard", required = true) @Schema(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空") @NotEmpty(message = "权限标识不能为空")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "Dashboard", required = true) @Schema(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空") @NotEmpty(message = "权限名称不能为空")
String name; String name;
@ApiModelProperty(value = "路径", example = "/dashboard") @Schema(description = "路径", example = "/dashboard")
String path; String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench") @Schema(description = "重定向", example = "/dashboard/workbench")
String redirect; String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT") @Schema(description = "组件", example = "LAYOUT")
String component; String component;
@ApiModelProperty(value = "图标", example = "ion:grid-outline") @Schema(description = "图标", example = "ion:grid-outline")
String icon; String icon;
@ApiModelProperty(value = "是否隐藏", example = "false") @Schema(description = "是否隐藏", example = "false")
Boolean hidden; Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true") @Schema(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0") @Schema(description = "父级ID", example = "0")
@Builder.Default @Builder.Default
String pid = "0"; String pid = "0";
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta; JSONObject meta;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.role; package com.yiring.auth.param.role;
import io.swagger.annotations.ApiModel; import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 角色信息入参类 * 角色信息入参类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@ApiModel("RoleParam") @Schema(name = "RoleParam")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,11 +30,15 @@ public class RoleParam implements Serializable { ...@@ -27,11 +30,15 @@ public class RoleParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6572751635422870217L; private static final long serialVersionUID = 6572751635422870217L;
@ApiModelProperty(value = "标识", example = "admin", required = true) @Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "标识", example = "admin")
@NotEmpty(message = "角色标识不能为空") @NotEmpty(message = "角色标识不能为空")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "管理员", required = true) @Parameter(description = "名称", example = "管理员")
@NotEmpty(message = "角色名称不能为空") @NotEmpty(message = "角色名称不能为空")
String name; String name;
} }
...@@ -6,10 +6,11 @@ import cn.dev33.satoken.stp.StpUtil; ...@@ -6,10 +6,11 @@ import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Resource;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
...@@ -22,14 +23,20 @@ import org.springframework.stereotype.Component; ...@@ -22,14 +23,20 @@ import org.springframework.stereotype.Component;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Component @Component
@RequiredArgsConstructor
public class Auths { public class Auths {
@Resource final UserRepository userRepository;
UserRepository userRepository;
/**
* 管理员角色标识
*/
public static final List<String> ADMIN_ROLES = List.of("admin", "super-admin", "platform-admin", "data-admin");
/** /**
* 根据 Token 获取用户信息 * 根据 Token 获取用户信息
* 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED} * 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED}
*
* @param token token * @param token token
* @return 用户信息 * @return 用户信息
*/ */
...@@ -61,4 +68,50 @@ public class Auths { ...@@ -61,4 +68,50 @@ public class Auths {
return getUserByToken(token); return getUserByToken(token);
} }
/**
* 踢出这个用户 id 所有登录状态(可能有多人重复登录了一个账号的情况)
*
* @param userId 用户 id
*/
public void logoutAll(String userId) {
List<String> tokens = StpUtil.getTokenValueListByLoginId(userId);
for (String token : tokens) {
StpUtil.logoutByTokenValue(token);
}
}
/**
* 判断用户是否为超级管理员
*
* @param userId 用户 ID
* @return 是否为管理员
*/
public boolean isAdmin(String userId) {
Optional<User> optional = userRepository.findById(userId);
return optional.filter(this::isAdmin).isPresent();
}
/**
* 检查用户是否为管理员(检查用户是否拥有包含 admin 字符的角色)
*
* @param user 用户
* @return 是否为管理员
*/
public boolean isAdmin(User user) {
return user
.getRoles()
.stream()
.anyMatch(role -> Boolean.TRUE.equals(role.getEnable()) && ADMIN_ROLES.contains(role.getUid()));
}
/**
* 检查当前登录用户是否为管理员
* {@link this.isAdmin}
*
* @return 是否为管理员
*/
public boolean checkLoginUserIsAdmin() {
return isAdmin(getLoginUser());
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.util; package com.yiring.auth.util;
import com.sun.istack.Nullable;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
import com.yiring.auth.vo.permission.MenuVo; import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import jakarta.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -28,6 +28,7 @@ public class Permissions { ...@@ -28,6 +28,7 @@ public class Permissions {
/** /**
* 将角色集合转换成 Vo 集合 * 将角色集合转换成 Vo 集合
*
* @param roles 角色集合 * @param roles 角色集合
* @return vos * @return vos
*/ */
...@@ -40,6 +41,7 @@ public class Permissions { ...@@ -40,6 +41,7 @@ public class Permissions {
/** /**
* 将权限集合转换成菜单树 * 将权限集合转换成菜单树
*
* @param permissions 权限集合 * @param permissions 权限集合
* @return 菜单树 * @return 菜单树
*/ */
...@@ -70,11 +72,35 @@ public class Permissions { ...@@ -70,11 +72,35 @@ public class Permissions {
} }
}); });
return roots; return new ArrayList<>(sortMenuTreeVo(roots));
}
/**
* 菜单树递归排序
*
* @param menus 菜单集合
* @return 排序后的菜单集合
*/
public static List<MenuVo> sortMenuTreeVo(@Nullable List<MenuVo> menus) {
return menus
.stream()
.sorted(
Comparator.comparing(
item -> item.getMeta().getIntValue("orderNo"),
Comparator.nullsFirst(Comparator.naturalOrder())
)
)
.peek(item -> {
if (Commons.notEmpty(item.getChildren())) {
item.setChildren(sortMenuTreeVo(item.getChildren()));
}
})
.toList();
} }
/** /**
* 将权限集合转换成 Vo 集合 * 将权限集合转换成 Vo 集合
*
* @param permissions 权限集合 * @param permissions 权限集合
* @return vos * @return vos
*/ */
...@@ -92,6 +118,7 @@ public class Permissions { ...@@ -92,6 +118,7 @@ public class Permissions {
/** /**
* 提取角色集合含有的权限去重结果 * 提取角色集合含有的权限去重结果
*
* @param roles 角色集合 * @param roles 角色集合
* @return 权限集合 * @return 权限集合
*/ */
...@@ -101,14 +128,16 @@ public class Permissions { ...@@ -101,14 +128,16 @@ public class Permissions {
.stream() .stream()
.map(Role::getPermissions) .map(Role::getPermissions)
.flatMap(Set::stream) .flatMap(Set::stream)
.distinct()
.sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length))) .sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length)))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* 根据 pid 构建树状权限集合 * 根据 pid 构建树状权限集合
*
* @param permissions 权限集合 * @param permissions 权限集合
* @param pid 权限父级 ID * @param pid 权限父级 ID
* @return 树状权限集合 * @return 树状权限集合
*/ */
public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) { public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.auth.vo.auth; package com.yiring.auth.vo.auth;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.*; import lombok.*;
...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("LoginVo") @Schema(name = "LoginVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,9 +26,9 @@ public class LoginVo implements Serializable { ...@@ -27,9 +26,9 @@ public class LoginVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456896L; private static final long serialVersionUID = -8690942241103456896L;
@ApiModelProperty(value = "用户 ID", example = "1") @Schema(description = "用户 ID", example = "1")
String userId; String userId;
@ApiModelProperty(value = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741") @Schema(description = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
String token; String token;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission; package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -14,13 +13,14 @@ import lombok.experimental.FieldDefaults; ...@@ -14,13 +13,14 @@ import lombok.experimental.FieldDefaults;
/** /**
* 菜单输出类 * 菜单输出类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("MenuVo") @Schema(name = "MenuVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -37,21 +37,24 @@ public class MenuVo implements Serializable { ...@@ -37,21 +37,24 @@ public class MenuVo implements Serializable {
@JsonIgnore @JsonIgnore
String pid; String pid;
@ApiModelProperty(value = "名称", example = "Dashboard") @Schema(description = "唯一标识", example = "Dashboard")
String uid;
@Schema(description = "名称", example = "Dashboard")
String name; String name;
@ApiModelProperty(value = "路径", example = "/dashboard") @Schema(description = "路径", example = "/dashboard")
String path; String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench") @Schema(description = "重定向", example = "/dashboard/workbench")
String redirect; String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT") @Schema(description = "组件", example = "LAYOUT")
String component; String component;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta; JSONObject meta;
@ApiModelProperty(value = "子权限") @Schema(description = "子权限")
List<MenuVo> children; List<MenuVo> children;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission; package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults; ...@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults;
/** /**
* 权限输出类 * 权限输出类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionVo") @Schema(name = "PermissionVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable { ...@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -9139328772148985141L; private static final long serialVersionUID = -9139328772148985141L;
@ApiModelProperty(value = "主键", example = "1") @Parameter(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "权限类型", example = "MENU") @Parameter(description = "权限类型", example = "MENU")
Permission.Type type; Permission.Type type;
@ApiModelProperty(value = "序号", example = "1") @Parameter(description = "序号", example = "1")
Integer serial; Integer serial;
@ApiModelProperty(value = "标识", example = "home") @Parameter(description = "标识", example = "home")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "主页") @Parameter(description = "名称", example = "主页")
String name; String name;
@ApiModelProperty(value = "路径", example = "/") @Parameter(description = "路径", example = "/")
String path; String path;
@ApiModelProperty(value = "组件", example = "/home") @Parameter(description = "组件", example = "/home")
String component; String component;
@ApiModelProperty(value = "图标", example = "menu") @Parameter(description = "图标", example = "menu")
String icon; String icon;
@ApiModelProperty(value = "是否隐藏", example = "false") @Parameter(description = "是否隐藏", example = "false")
Boolean hidden; Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true") @Parameter(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0") @Parameter(description = "父级ID", example = "0")
String pid; String pid;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta; JSONObject meta;
@ApiModelProperty(value = "子权限") @Parameter(description = "子权限")
List<PermissionVo> children; List<PermissionVo> children;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.role; package com.yiring.auth.vo.role;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
...@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults; ...@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults;
/** /**
* 角色响应类 * 角色响应类
*
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/25 17:09 * 2022/3/25 17:09
*/ */
@ApiModel("RoleVo") @Schema(name = "RoleVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,15 +28,15 @@ public class RoleVo implements Serializable { ...@@ -28,15 +28,15 @@ public class RoleVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -9154497137563970840L; private static final long serialVersionUID = -9154497137563970840L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "标识", example = "admin") @Schema(description = "标识", example = "admin")
String uid; String uid;
@ApiModelProperty(value = "名称", example = "系统管理员") @Schema(description = "名称", example = "系统管理员")
String name; String name;
@ApiModelProperty("权限") @Schema(description = "权限")
List<PermissionVo> permissions; List<PermissionVo> permissions;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults; ...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserInfo") @Schema(name = "UserInfo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,25 +28,28 @@ public class UserInfoVo implements Serializable { ...@@ -28,25 +28,28 @@ public class UserInfoVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -5319037883240327088L; private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String userId; String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "手机号", example = "15616260195")
String mobile;
@Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "介绍", example = "系统管理员") @Schema(description = "介绍", example = "系统管理员")
String desc; String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty("角色") @Schema(description = "角色")
@Builder.Default @Builder.Default
List<RoleVo> roles = new ArrayList<>(0); List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench") @Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath; String homePath;
} }
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults; ...@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserInfo") @Schema(name = "UserInfo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable { ...@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -5319037883240327088L; private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String userId; String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "介绍", example = "系统管理员") @Schema(description = "介绍", example = "系统管理员")
String desc; String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty("角色") @Schema(description = "角色")
@Builder.Default @Builder.Default
List<RoleVo> roles = new ArrayList<>(0); List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench") @Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath; String homePath;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.user; package com.yiring.auth.vo.user;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
...@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults; ...@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults;
/** /**
* 用户信息 * 用户信息
*
* @author ifzm * @author ifzm
* 2022/03/03 10:35 * 2022/03/03 10:35
**/ **/
@ApiModel("UserVo") @Schema(name = "UserVo")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -26,39 +26,39 @@ public class UserVo implements Serializable { ...@@ -26,39 +26,39 @@ public class UserVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -2184378273450466835L; private static final long serialVersionUID = -2184378273450466835L;
@ApiModelProperty(value = "主键", example = "1") @Schema(description = "主键", example = "1")
String id; String id;
@ApiModelProperty(value = "真实姓名", example = "超级用户") @Schema(description = "真实姓名", example = "超级用户")
String realName; String realName;
@ApiModelProperty(value = "用户名", example = "admin") @Schema(description = "用户名", example = "admin")
String username; String username;
@ApiModelProperty(value = "手机号", example = "13012345678") @Schema(description = "手机号", example = "13012345678")
String mobile; String mobile;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com") @Schema(description = "邮箱", example = "developer@yiring.com")
String email; String email;
@ApiModelProperty(value = "职称", example = "系统管理员") @Schema(description = "职称", example = "系统管理员")
String title; String title;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg") @Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar; String avatar;
@ApiModelProperty(value = "是否启用", example = "true") @Schema(description = "是否启用", example = "true")
Boolean enabled; Boolean enabled;
@ApiModelProperty(value = "是否删除", example = "false") @Schema(description = "是否删除", example = "false")
Boolean deleted; Boolean deleted;
@ApiModelProperty(value = "最后登录IP地址", example = "127.0.0.1") @Schema(description = "最后登录IP地址", example = "127.0.0.1")
String lastLoginIp; String lastLoginIp;
@ApiModelProperty(value = "最后登录时间", example = "2022-10-24 10:24:00") @Schema(description = "最后登录时间", example = "2022-10-24 10:24:00")
LocalDateTime lastLoginTime; LocalDateTime lastLoginTime;
@ApiModelProperty(value = "最后登录时间", example = "2022-01-01 00:00:00") @Schema(description = "最后登录时间", example = "2022-01-01 00:00:00")
LocalDateTime createTime; LocalDateTime createTime;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.auth; package com.yiring.auth.web.auth;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil; import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository; import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.param.auth.LoginParam; import com.yiring.auth.param.auth.LoginParam;
import com.yiring.auth.param.auth.RegisterParam; import com.yiring.auth.param.auth.RegisterParam;
import com.yiring.auth.param.auth.SafeParam;
import com.yiring.auth.util.Auths;
import com.yiring.auth.vo.auth.LoginVo; import com.yiring.auth.vo.auth.LoginVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
...@@ -37,100 +41,124 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -37,100 +41,124 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -99) name = "Auth",
@Api(tags = "身份认证", description = "Auth") description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@RestController @RestController
@RequestMapping("/auth/") @RequestMapping("/auth/")
@RequiredArgsConstructor
public class AuthController { public class AuthController {
@Resource final Auths auths;
UserRepository userRepository; final UserRepository userRepository;
@ApiOperation(value = "注册") @Operation(summary = "注册")
@PostMapping("register") @PostMapping(value = "register")
public Result<String> register(@Valid RegisterParam param) { public Result<String> register(@ParameterObject @Validated RegisterParam param) {
// 检查用户名是否存在 // 检查用户名是否存在
long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build())); long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "用户名已存在"); throw BusinessException.i18n("Code.100000");
} }
// 检查手机号是否存在 // 检查手机号是否存在
count = userRepository.count(Example.of(User.builder().mobile(param.getMobile()).build())); count = userRepository.count(Example.of(User.builder().mobile(param.getMobile()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "手机号已存在"); throw BusinessException.i18n("Code.100001");
} }
// 检查邮箱是否存在 // 检查邮箱是否存在
if (StrUtil.isNotBlank(param.getEmail())) { if (StrUtil.isNotBlank(param.getEmail())) {
count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build())); count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build()));
if (count > 0) { if (count > 0) {
return Result.no(Status.BAD_REQUEST, "邮箱已存在"); throw BusinessException.i18n("Code.100002");
} }
} }
// 构建用户信息写入数据库 // 构建用户信息写入数据库
User user = User User user = User
.builder() .builder()
.introduction(param.getIntroduction())
.avatar(param.getAvatar()) .avatar(param.getAvatar())
.mobile(param.getMobile()) .mobile(param.getMobile())
.realName(param.getRealName()) .realName(param.getRealName())
.username(param.getUsername()) .username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword())) .password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable()) .enabled(param.getEnable())
.deleted(Boolean.FALSE)
.createTime(LocalDateTime.now())
.build(); .build();
userRepository.saveAndFlush(user); userRepository.saveAndFlush(user);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "登录") @Operation(summary = "登录")
@PostMapping("login") @PostMapping("login")
public Result<LoginVo> login(@Valid LoginParam param, HttpServletRequest request) { public Result<LoginVo> login(@ParameterObject @Validated LoginParam param, HttpServletRequest request) {
String details = "账号密码错误";
// 查询用户信息是否匹配 // 查询用户信息是否匹配
User user = userRepository.findByAccount(param.getAccount()); User user = userRepository.findByAccount(param.getAccount());
if (user == null) { if (user == null) {
return Result.no(Status.BAD_REQUEST, details); throw BusinessException.i18n("Code.100003");
} }
// 检查密码 // 检查密码
String cps = SaSecureUtil.sha256(param.getPassword()); String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) { if (!cps.equals(user.getPassword())) {
return Result.no(Status.BAD_REQUEST, details); throw BusinessException.i18n("Code.100003");
} }
// 检查用户是否已被删除 // 检查用户是否已被删除
if (!Boolean.FALSE.equals(user.getDeleted())) { if (user.getDeleteTime() != null) {
return Result.no(Status.FORBIDDEN, "用户被禁用, 请联系管理员"); throw BusinessException.i18n("Code.100004");
} }
// 检查用户是否被允许登录 // 检查用户是否被允许登录
if (!Boolean.TRUE.equals(user.getEnabled())) { if (!Boolean.TRUE.equals(user.getEnabled())) {
return Result.no(Status.FORBIDDEN, "用户被禁止登录, 请联系管理员"); throw BusinessException.i18n("Code.100005");
} }
// 登录
StpUtil.login(user.getId());
// 更新用户信息 // 更新用户信息
user.setLastLoginIp(Commons.getClientIpAddress(request)); user.setLastLoginIp(Commons.getClientIpAddress(request));
user.setLastLoginTime(LocalDateTime.now()); user.setLastLoginTime(LocalDateTime.now());
userRepository.saveAndFlush(user); userRepository.saveAndFlush(user);
// 登录
StpUtil.login(user.getId());
// 构建用户所需信息 // 构建用户所需信息
LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build(); LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build();
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "登出") @Operation(summary = "检查登录")
@GetMapping("valid")
public Result<Boolean> valid() {
return Result.ok(StpUtil.isLogin());
}
@Operation(summary = "登出")
@GetMapping("logout") @GetMapping("logout")
public Result<String> logout() { public Result<String> logout() {
StpUtil.logout(); StpUtil.logout();
return Result.ok(); return Result.ok();
} }
/**
* 二次安全校验,搭配 @SaCheckSafe 实现对关键数据不可逆操作前的二次确认
* 默认安全时间: 120s
*
* @param param 用户密码
* @link { <a href="https://sa-token.dev33.cn/doc.html#/up/safe-auth">...</a> }
*/
@SaCheckLogin
@Operation(summary = "安全验证")
@GetMapping("safe")
public Result<String> safe(@ParameterObject @Validated SafeParam param) {
User user = auths.getLoginUser();
if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) {
StpUtil.openSafe(360);
return Result.ok();
}
return Result.no();
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.permission; package com.yiring.auth.web.sys.permission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam; import com.yiring.auth.param.permission.PermissionParam;
...@@ -11,26 +9,27 @@ import com.yiring.auth.util.Permissions; ...@@ -11,26 +9,27 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.PermissionVo; import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam; import com.yiring.common.param.IdParam;
import com.yiring.common.param.OptionalPidParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.param.PidParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 系统权限管理控制器 * 系统权限管理控制器
...@@ -42,60 +41,56 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -42,60 +41,56 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -97) name = "权限管理",
@Api(tags = "权限管理", description = "Permission") description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@RestController @RestController
@RequestMapping("/manage/permission/") @RequestMapping("/sys/permission/")
@RequiredArgsConstructor
public class PermissionController { public class PermissionController {
@Resource final PermissionRepository permissionRepository;
PermissionRepository permissionRepository;
@ApiOperation(value = "新增") @Operation(summary = "新增")
@PostMapping("add") @PostMapping("add")
public Result<String> add(@Valid PermissionParam param) { public Result<String> add(@RequestBody @Validated({ Group.Add.class }) PermissionParam param) {
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复"); throw BusinessException.i18n("Code.1001");
} }
Permission entity = new Permission(); Permission entity = new Permission();
BeanUtils.copyProperties(param, entity); save(entity, param);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "修改") @Operation(summary = "修改")
@PostMapping("modify") @PostMapping("modify")
public Result<String> modify(@Valid PermissionParam param, @Valid IdParam idParam) { public Result<String> modify(@RequestBody @Validated({ Group.Edit.class }) PermissionParam param) {
Optional<Permission> optional = permissionRepository.findById(idParam.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission entity = optional.get(); Permission entity = optional.get();
if (!entity.getUid().equals(param.getUid())) { if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复 // 仅当修改了角色标识时才检查重复
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "权限标识重复"); throw BusinessException.i18n("Code.1001");
} }
} }
BeanUtils.copyProperties(param, entity); save(entity, param);
entity.setTree(getTreeNode(param.getPid()));
entity.setMeta(JSON.parseObject(param.getMeta()));
permissionRepository.saveAndFlush(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@Valid IdParam param) { public Result<String> remove(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission entity = optional.get(); Permission entity = optional.get();
...@@ -103,31 +98,33 @@ public class PermissionController { ...@@ -103,31 +98,33 @@ public class PermissionController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "查询") @Operation(summary = "查询")
@GetMapping("find") @GetMapping("find")
public Result<PermissionVo> find(@Valid IdParam param) { public Result<PermissionVo> find(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Permission permission = optional.get();
PermissionVo vo = new PermissionVo(); PermissionVo vo = new PermissionVo();
BeanUtils.copyProperties(optional.get(), vo); BeanUtils.copyProperties(optional.get(), vo, Permission.Fields.meta);
vo.setMeta(permission.getMetaJson());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "分页查询") @Operation(summary = "分页查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<PermissionVo>> page(@Valid PageParam param) { public Result<PageVo<PermissionVo>> page(@ParameterObject @Validated PageParam param) {
Page<Permission> page = permissionRepository.findAll(PageParam.toPageable(param)); Page<Permission> page = permissionRepository.findAll(PageParam.toPageable(param));
List<PermissionVo> data = Permissions.toPermissionVos(page.toList()); List<PermissionVo> data = Permissions.toPermissionVos(page.toList());
PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements()); PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "树结构查询") @Operation(summary = "树结构查询")
@GetMapping(value = "tree") @GetMapping(value = "tree")
public Result<ArrayList<PermissionVo>> tree(OptionalPidParam param) { public Result<ArrayList<PermissionVo>> tree(@ParameterObject @Validated(Group.Optional.class) PidParam param) {
List<Permission> permissions = permissionRepository.findAll(); List<Permission> permissions = permissionRepository.findAll();
List<PermissionVo> vos = Permissions.toTree( List<PermissionVo> vos = Permissions.toTree(
permissions, permissions,
...@@ -138,6 +135,7 @@ public class PermissionController { ...@@ -138,6 +135,7 @@ public class PermissionController {
/** /**
* 根据父级 ID 获取当前树节点标识 * 根据父级 ID 获取当前树节点标识
*
* @param pid 父级 ID * @param pid 父级 ID
* @return 树节点标识 * @return 树节点标识
*/ */
...@@ -152,6 +150,7 @@ public class PermissionController { ...@@ -152,6 +150,7 @@ public class PermissionController {
/** /**
* 检查是否存在已有相同标识的权限 * 检查是否存在已有相同标识的权限
*
* @param uid 权限标识 * @param uid 权限标识
* @return 是否存在 * @return 是否存在
*/ */
...@@ -159,4 +158,16 @@ public class PermissionController { ...@@ -159,4 +158,16 @@ public class PermissionController {
Permission entity = Permission.builder().uid(uid).build(); Permission entity = Permission.builder().uid(uid).build();
return permissionRepository.count(Example.of(entity)) > 0; return permissionRepository.count(Example.of(entity)) > 0;
} }
/**
* 新增或修改权限菜单
*
* @param entity 实体对象
* @param param 参数
*/
private void save(Permission entity, PermissionParam param) {
BeanUtils.copyProperties(param, entity, Permission.Fields.meta);
entity.setTree(getTreeNode(param.getPid()));
permissionRepository.saveAndFlush(entity);
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.role; package com.yiring.auth.web.sys.role;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.role.Role; import com.yiring.auth.domain.role.Role;
...@@ -11,17 +10,21 @@ import com.yiring.auth.util.Permissions; ...@@ -11,17 +10,21 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.role.RoleVo; import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam; import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam; import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import lombok.RequiredArgsConstructor;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
...@@ -41,24 +44,24 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -41,24 +44,24 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -96) name = "角色管理",
@Api(tags = "角色管理", description = "Role") description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@RestController @RestController
@RequestMapping("/manage/role/") @RequestMapping("/sys/role/")
@RequiredArgsConstructor
public class RoleController { public class RoleController {
@Resource final RoleRepository roleRepository;
RoleRepository roleRepository; final PermissionRepository permissionRepository;
@Resource @Operation(summary = "新增")
PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@PostMapping("add") @PostMapping("add")
public Result<String> add(@Valid RoleParam param) { public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) RoleParam param) {
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复"); throw BusinessException.i18n("Code.1002");
} }
Role entity = new Role(); Role entity = new Role();
...@@ -67,19 +70,19 @@ public class RoleController { ...@@ -67,19 +70,19 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "修改") @Operation(summary = "修改")
@PostMapping("modify") @PostMapping("modify")
public Result<String> modify(@Valid RoleParam param, @Valid IdParam idParam) { public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) RoleParam param) {
Optional<Role> optional = roleRepository.findById(idParam.getId()); Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Role entity = optional.get(); Role entity = optional.get();
if (!entity.getUid().equals(param.getUid())) { if (!entity.getUid().equals(param.getUid())) {
// 仅当修改了角色标识时才检查重复 // 仅当修改了角色标识时才检查重复
if (has(param.getUid())) { if (has(param.getUid())) {
return Result.no(Status.BAD_REQUEST, "角色标识重复"); throw BusinessException.i18n("Code.1002");
} }
} }
...@@ -88,21 +91,20 @@ public class RoleController { ...@@ -88,21 +91,20 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "分配权限") @Operation(summary = "分配权限")
@PostMapping("assign") @PostMapping("assign")
public Result<String> assign(@Valid IdParam idParam, @Valid IdsParam idsParam) { public Result<String> assign(
@ParameterObject @Validated IdParam idParam,
@ParameterObject @Validated IdsParam idsParam
) {
Optional<Role> optional = roleRepository.findById(idParam.getId()); Optional<Role> optional = roleRepository.findById(idParam.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
// 查询权限集合 // 查询权限集合
Set<String> ids = idsParam.toIds(); Collection<Serializable> ids = idsParam.toIds();
Set<Permission> permissions = permissionRepository Set<Permission> permissions = new HashSet<>(permissionRepository.findAllById(ids));
.findAll()
.stream()
.filter(permission -> ids.contains(permission.getId()))
.collect(Collectors.toSet());
Role entity = optional.get(); Role entity = optional.get();
entity.setPermissions(permissions); entity.setPermissions(permissions);
...@@ -110,25 +112,20 @@ public class RoleController { ...@@ -110,25 +112,20 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@Valid IdParam param) { public Result<String> remove(@ParameterObject @Validated IdsParam param) {
Optional<Role> optional = roleRepository.findById(param.getId()); List<Role> roles = roleRepository.findAllById(param.toIds());
if (optional.isEmpty()) { roleRepository.deleteAll(roles);
return Result.no(Status.NOT_FOUND);
}
Role entity = optional.get();
roleRepository.delete(entity);
return Result.ok(); return Result.ok();
} }
@ApiOperation(value = "查询") @Operation(summary = "查询")
@GetMapping("find") @GetMapping("find")
public Result<RoleVo> find(@Valid IdParam param) { public Result<RoleVo> find(@ParameterObject @Validated IdParam param) {
Optional<Role> optional = roleRepository.findById(param.getId()); Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND); throw Status.NOT_FOUND.exception();
} }
Role entity = optional.get(); Role entity = optional.get();
...@@ -138,16 +135,16 @@ public class RoleController { ...@@ -138,16 +135,16 @@ public class RoleController {
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "分页查询") @Operation(summary = "分页查询")
@GetMapping("page") @GetMapping("page")
public Result<PageVo<RoleVo>> page(@Valid PageParam param) { public Result<PageVo<RoleVo>> page(@ParameterObject @Validated PageParam param) {
Page<Role> page = roleRepository.findAll(PageParam.toPageable(param)); Page<Role> page = roleRepository.findAll(PageParam.toPageable(param));
List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet())); List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet()));
PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements()); PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo); return Result.ok(vo);
} }
@ApiOperation(value = "选项查询") @Operation(summary = "选项查询")
@GetMapping("selector") @GetMapping("selector")
public Result<ArrayList<RoleVo>> selector() { public Result<ArrayList<RoleVo>> selector() {
List<Role> roles = roleRepository.findAll(); List<Role> roles = roleRepository.findAll();
...@@ -157,6 +154,7 @@ public class RoleController { ...@@ -157,6 +154,7 @@ public class RoleController {
/** /**
* 检查是否存在已有相同标识的角色 * 检查是否存在已有相同标识的角色
*
* @param uid 角色标识 * @param uid 角色标识
* @return 是否存在 * @return 是否存在
*/ */
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.user;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.vo.user.UserVo;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.util.Commons;
import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 系统用户控制器
*
* @author Jim
* @version 0.1
* 2022/1/24 14:13
*/
@Slf4j
@Validated
@Tag(
name = "用户管理",
description = "User",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-97") }) }
)
@RestController
@RequestMapping("/sys/user/")
@RequiredArgsConstructor
public class UserController {
final UserRepository userRepository;
final RoleRepository roleRepository;
@Operation(summary = "分配角色")
@PostMapping("assign")
public Result<String> assign(
@ParameterObject @Validated IdParam idParam,
@ParameterObject @Validated IdsParam idsParam
) {
Optional<User> optional = userRepository.findById(idParam.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
// 查询角色集合
Collection<Serializable> ids = idsParam.toIds();
Set<Role> roles = new HashSet<>(roleRepository.findAllById(ids));
User entity = optional.get();
entity.setRoles(roles);
userRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<UserVo>> page(@ParameterObject @Validated PageParam param) {
Page<User> page = userRepository.findAll(PageParam.toPageable(param));
List<UserVo> data = page.get().map(user -> Commons.transform(user, UserVo.class)).collect(Collectors.toList());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.user; package com.yiring.auth.web.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.util.Auths; import com.yiring.auth.util.Auths;
import com.yiring.auth.util.Permissions; import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.MenuVo; import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo; import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.auth.vo.user.UserVo;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import io.swagger.v3.oas.annotations.Operation;
import com.yiring.common.param.IdParam; import io.swagger.v3.oas.annotations.extensions.Extension;
import com.yiring.common.param.IdsParam; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import com.yiring.common.param.PageParam; import io.swagger.v3.oas.annotations.tags.Tag;
import com.yiring.common.util.Commons;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
...@@ -46,39 +32,35 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -46,39 +32,35 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -95) name = "用户信息",
@Api(tags = "用户管理", description = "User") description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@RestController @RestController
@RequestMapping("/user/") @RequestMapping("/user/")
public class UserController { @RequiredArgsConstructor
public class UserViewController {
@Resource final Auths auths;
Auths auths;
@Resource @Operation(summary = "获取登录用户信息")
UserRepository userRepository;
@Resource
RoleRepository roleRepository;
@ApiOperation(value = "获取登录用户信息")
@GetMapping("getUserInfo") @GetMapping("getUserInfo")
public Result<UserInfoVo> getUserInfo() { public Result<UserInfoVo> getUserInfo() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
UserInfoVo userInfoVo = UserInfoVo UserInfoVo userInfoVo = UserInfoVo
.builder() .builder()
.userId(user.getId()) .userId(user.getId())
.mobile(user.getMobile())
.username(user.getUsername()) .username(user.getUsername())
.realName(user.getRealName()) .realName(user.getRealName())
.avatar(user.getAvatar()) .avatar(user.getAvatar())
.desc(user.getIntroduction())
.roles(Permissions.toRoleVos(user.getRoles())) .roles(Permissions.toRoleVos(user.getRoles()))
.build(); .build();
return Result.ok(userInfoVo); return Result.ok(userInfoVo);
} }
@ApiOperation(value = "获取用户菜单") @Operation(summary = "获取用户菜单")
@GetMapping("getMenuList") @GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() { public Result<ArrayList<MenuVo>> getMenuList() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
...@@ -91,7 +73,7 @@ public class UserController { ...@@ -91,7 +73,7 @@ public class UserController {
return Result.ok((ArrayList<MenuVo>) vos); return Result.ok((ArrayList<MenuVo>) vos);
} }
@ApiOperation(value = "获取用户权限") @Operation(summary = "获取用户权限")
@GetMapping("getPermCode") @GetMapping("getPermCode")
public Result<ArrayList<String>> getPermCode() { public Result<ArrayList<String>> getPermCode() {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
...@@ -99,36 +81,4 @@ public class UserController { ...@@ -99,36 +81,4 @@ public class UserController {
List<String> codes = permissions.stream().map(Permission::getUid).collect(Collectors.toList()); List<String> codes = permissions.stream().map(Permission::getUid).collect(Collectors.toList());
return Result.ok((ArrayList<String>) codes); return Result.ok((ArrayList<String>) codes);
} }
@ApiOperation(value = "分配角色")
@PostMapping("/manage/assign")
public Result<String> assign(@Valid IdParam idParam, @Valid IdsParam idsParam) {
Optional<User> optional = userRepository.findById(idParam.getId());
if (optional.isEmpty()) {
return Result.no(Status.NOT_FOUND);
}
// 查询权限集合
Set<String> ids = idsParam.toIds();
Set<Role> roles = roleRepository
.findAll()
.stream()
.filter(role -> ids.contains(role.getId()))
.collect(Collectors.toSet());
User entity = optional.get();
entity.setRoles(roles);
userRepository.saveAndFlush(entity);
return Result.ok();
}
@ApiOperation(value = "分页查询")
@GetMapping("/manage/page")
public Result<PageVo<UserVo>> page(@Valid PageParam param) {
Page<User> page = userRepository.findAll(PageParam.toPageable(param));
List<UserVo> data = page.get().map(user -> Commons.transform(user, UserVo.class)).collect(Collectors.toList());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
} }
# Sa-Token配置 # Sa-Token配置
sa-token: sa-token:
# token名称 (同时也是cookie名称) # token名称 (同时也是cookie名称)
token-name: Authorization token-name: App-Token
# token有效期,单位s 默认30天, -1代表永不过期 # token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000 timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
......
Code.1000=\u7528\u6237\u4E0D\u5B58\u5728
Code.1001=\u6743\u9650\u6807\u8BC6\u91CD\u590D
Code.1002=\u89D2\u8272\u6807\u8BC6\u91CD\u590D
Code.10000=\u4E8C\u7EA7\u8BA4\u8BC1\u6821\u9A8C\u5931\u8D25
Code.10001=\u89D2\u8272\u6743\u9650\u4E0D\u8DB3
Code.10002=\u6743\u9650\u4E0D\u8DB3
Code.10003=\u672A\u6388\u6743
Code.100000=\u7528\u6237\u540D\u5DF2\u5B58\u5728
Code.100001=\u624B\u673A\u53F7\u5DF2\u5B58\u5728
Code.100002=\u90AE\u7BB1\u5DF2\u5B58\u5728
Code.100003=\u8D26\u53F7\u5BC6\u7801\u9519\u8BEF
Code.100004=\u7528\u6237\u88AB\u7981\u7528, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.100005=\u7528\u6237\u88AB\u7981\u6B62\u767B\u5F55, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.1000=\u7528\u6237\u4E0D\u5B58\u5728
Code.1001=\u6743\u9650\u6807\u8BC6\u91CD\u590D
Code.1002=\u89D2\u8272\u6807\u8BC6\u91CD\u590D
Code.10000=\u4E8C\u7EA7\u8BA4\u8BC1\u6821\u9A8C\u5931\u8D25
Code.10001=\u89D2\u8272\u6743\u9650\u4E0D\u8DB3
Code.10002=\u6743\u9650\u4E0D\u8DB3
Code.10003=\u672A\u6388\u6743
Code.100000=\u7528\u6237\u540D\u5DF2\u5B58\u5728
Code.100001=\u624B\u673A\u53F7\u5DF2\u5B58\u5728
Code.100002=\u90AE\u7BB1\u5DF2\u5B58\u5728
Code.100003=\u8D26\u53F7\u5BC6\u7801\u9519\u8BEF
Code.100004=\u7528\u6237\u88AB\u7981\u7528, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Code.100005=\u7528\u6237\u88AB\u7981\u6B62\u767B\u5F55, \u8BF7\u8054\u7CFB\u7BA1\u7406\u5458
Status.OK=OK
Status.NON_AUTHORITATIVE_INFORMATION=Non-Authoritative Information
Status.BAD_REQUEST=Bad Request
Status.UNAUTHORIZED=Unauthorized
Status.FORBIDDEN=Forbidden
Status.NOT_FOUND=Not Found
Status.METHOD_NOT_ALLOWED=Method Not Allowed
Status.EXPECTATION_FAILED=Expectation Failed
Status.INTERNAL_SERVER_ERROR=Internal Server Error
Status.UNKNOWN_ERROR=Unknown Error
Status.NOT_IMPLEMENTED=Not Implemented
Status.BAD_GATEWAY=Bad Gateway
Status.SERVICE_UNAVAILABLE=Service Unavailable
Status.OK=\u6210\u529F
Status.NON_AUTHORITATIVE_INFORMATION=\u8BA4\u8BC1\u5931\u8D25
Status.BAD_REQUEST=\u8BF7\u6C42\u5931\u8D25
Status.UNAUTHORIZED=\u51ED\u8BC1\u8FC7\u671F
Status.FORBIDDEN=\u7981\u6B62\u8BBF\u95EE
Status.NOT_FOUND=\u627E\u4E0D\u5230\u8D44\u6E90
Status.METHOD_NOT_ALLOWED=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u7C7B\u578B
Status.EXPECTATION_FAILED=\u65E0\u6548\u53C2\u6570
Status.INTERNAL_SERVER_ERROR=\u670D\u52A1\u5668\u9519\u8BEF
Status.UNKNOWN_ERROR=\u672A\u77E5\u9519\u8BEF
Status.NOT_IMPLEMENTED=API \u672A\u5B9E\u73B0
Status.BAD_GATEWAY=\u670D\u52A1\u5F02\u5E38
Status.SERVICE_UNAVAILABLE=\u670D\u52A1\u6682\u505C
dependencies { dependencies {
implementation project(":basic-common:util") implementation project(":basic-common:util")
implementation project(":basic-common:i18n")
implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 本地依赖 // 本地依赖
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar']) implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-extra // hutool-extra
implementation "cn.hutool:hutool-extra:${hutoolVersion}" implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// fastjson // fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
// JTS 几何对象操作库 // JTS 几何对象操作库
implementation "org.locationtech.jts:jts-core:${jtsVersion}" implementation "org.locationtech.jts:jts-core:${jtsVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
// hypersistence-utils-hibernate-60
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.n52.jackson/jackson-datatype-jts/1.2.10 // https://mvnrepository.com/artifact/org.n52.jackson/jackson-datatype-jts/1.2.10
implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") { implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") {
exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.locationtech.jts' exclude group: 'org.locationtech.jts'
} }
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
} }
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.annotation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.lang.annotation.*;
import org.springframework.core.annotation.AliasFor;
import org.springframework.http.MediaType;
/**
* 下载响应注解
*
* @author Jim
* @version 0.1
* 2023/1/12 11:22
*/
@SuppressWarnings({ "unused" })
@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@ApiResponse
public @interface DownloadResponse {
@AliasFor(annotation = ApiResponse.class)
String responseCode() default "200";
@AliasFor(annotation = ApiResponse.class)
String description() default "OK";
@AliasFor(annotation = ApiResponse.class)
Content content() default @Content(
schema = @Schema(type = "file", format = "binary"),
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE
);
}
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.aspect; package com.yiring.common.aspect;
import cn.hutool.extra.servlet.ServletUtil; import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter; import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.servlet.http.HttpServletRequest; import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
...@@ -34,6 +35,11 @@ public class RequestAspect { ...@@ -34,6 +35,11 @@ public class RequestAspect {
@Value("${debug}") @Value("${debug}")
Boolean debug; Boolean debug;
/**
* 白名单(忽略)
*/
List<String> IGNORE_LIST = List.of("/swagger-resources", "/error", "/v3/api-docs");
@Pointcut( @Pointcut(
"@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)" "@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)"
) )
...@@ -42,20 +48,33 @@ public class RequestAspect { ...@@ -42,20 +48,33 @@ public class RequestAspect {
@Around("apiPointCut()") @Around("apiPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest(); HttpServletRequest request = getRequest();
// 放行白名单
for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) {
return point.proceed();
}
}
// 计算接口执行耗时 // 计算接口执行耗时
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Object result = point.proceed(); Object result = point.proceed();
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
// 计算接口处理消耗时间,格式化
String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME); String timestamp = LocalDateTime.now().format(DateFormatter.DATE_TIME);
String times = String.format("%.3fs", (double) (end - start) / 1000); String times = String.format("%.3fs", (double) (end - start) / 1000);
// Print Request Log (Optional Replace: MDC) // 获取接口请求扩展信息,Header, Params
String extra = ""; String extra = "";
if (Boolean.TRUE.equals(debug)) { if (Boolean.TRUE.equals(debug)) {
String headers = JSONObject.toJSONString(ServletUtil.getHeaderMap(request), SerializerFeature.PrettyFormat); String headers = JSONObject.toJSONString(
String params = JSONObject.toJSONString(ServletUtil.getParamMap(request), SerializerFeature.PrettyFormat); JakartaServletUtil.getHeaderMap(request),
JSONWriter.Feature.PrettyFormat
);
String params = JSONObject.toJSONString(
JakartaServletUtil.getParamMap(request),
JSONWriter.Feature.PrettyFormat
);
extra += String.format("\nHeaders: %s", headers); extra += String.format("\nHeaders: %s", headers);
extra += String.format("\nParams: %s", params); extra += String.format("\nParams: %s", params);
if (result instanceof Result) { if (result instanceof Result) {
...@@ -67,20 +86,26 @@ public class RequestAspect { ...@@ -67,20 +86,26 @@ public class RequestAspect {
); );
} }
} }
// 获取接口处理返回的状态码,设置接口响应时间和耗时信息
int status = 200;
if (result instanceof Result) {
((Result<?>) result).setTimestamp(timestamp);
((Result<?>) result).setTimes(times);
status = ((Result<?>) result).getStatus();
}
// 打印请求日志 (Optional Replace: MDC, Trace)
log.info( log.info(
"[Request] Method: {}, URL: {}, IP: {}, Times: {}{}", "[Request] Method: {}, URL: {}, Status: {}, IP: {}, Times: {}{}",
request.getMethod(), request.getMethod(),
request.getRequestURL(), request.getRequestURL(),
status,
Commons.getClientIpAddress(request), Commons.getClientIpAddress(request),
times, times,
extra extra
); );
if (result instanceof Result) {
((Result<?>) result).setTimestamp(timestamp);
((Result<?>) result).setTimes(times);
}
return result; return result;
} }
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.config;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.exception.FailStatusException;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.aspectj.bridge.AbortException;
import org.hibernate.validator.internal.engine.ConstraintViolationImpl;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 核心异常拦截处理
*
* @author Jim
* @version 0.1
* 2023/1/12 14:06
*/
@Slf4j
@Order(0)
@RestControllerAdvice
@RequiredArgsConstructor
public class CoreExceptionHandler {
final I18n i18n;
/**
* 参数校验异常
*
* @param e 异常信息
* @return 统一的校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(
{ BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
)
public Result<String> validFailHandler(Exception e) {
String details = null;
if (e instanceof ConstraintViolationException) {
details = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
} else {
BindingResult result = null;
if (e instanceof MethodArgumentNotValidException) {
result = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
result = ((BindException) e).getBindingResult();
}
if (result != null) {
ObjectError error = result.getAllErrors().iterator().next();
if (error instanceof FieldError fieldError) {
// 构建明确的字段错误提示, 例如: id 不能为 null, 如果自己填写了 message 则不追加 field 字段前缀
ConstraintViolationImpl<?> violation = error.unwrap(ConstraintViolationImpl.class);
String template = violation.getMessageTemplate();
String prefix = "";
// 如果是模板字符串, 则在消息前添加字段提示
if (template.contains("{") && template.contains("}")) {
prefix = "参数" + fieldError.getField();
}
details = prefix + i18n.get(fieldError);
} else {
details = i18n.get(error);
}
}
}
return Result.no(Status.EXPECTATION_FAILED, details);
}
/**
* 参数读取解析失败异常
* eg: 例如参数使用 RequestBody 接收,但是传了个空字符串,导致解析失败
*
* @param e 异常
* @return 校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public Result<String> httpMessageNotReadableExceptionHandler(Exception e) {
log.warn(e.getMessage(), e);
return Result.no(Status.EXPECTATION_FAILED);
}
/**
* 不支持的HttpMethod异常
*
* @param e 异常信息
* @return 异常信息反馈 {@link Status#METHOD_NOT_ALLOWED
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<String> httpRequestMethodNotSupportedErrorHandler(Exception e) {
return Result.no(Status.METHOD_NOT_ALLOWED, e.getMessage());
}
/**
* 自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<String> businessExceptionHandler(BusinessException e) {
return Result.no(e.getStatus(), e.getMessage());
}
/**
* 失败状态异常
*/
@ExceptionHandler(FailStatusException.class)
public Result<String> failStatusExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus(), e.getMessage());
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler({ ClientAbortException.class, AbortException.class, HttpMessageNotWritableException.class })
public void ignoreExceptionHandler() {}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.config; package com.yiring.common.config;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,12 +19,71 @@ import org.springframework.context.annotation.Configuration; ...@@ -17,12 +19,71 @@ import org.springframework.context.annotation.Configuration;
@Data @Data
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration @Configuration("env.config")
@ConfigurationProperties(prefix = "env") @ConfigurationProperties(prefix = "env")
public class EnvConfig { public class EnvConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1017213697767634790L;
/** /**
* host,用来共享一些资源(如:数据库、文件存储等相关的依赖源) * host,用来共享一些资源(如:数据库、文件存储等相关的依赖源)
*/ */
String host; String host;
/**
* 是否为生产环境
*/
boolean prod;
/**
* FFmpeg 配置
*/
FFmpeg ffmpeg;
/**
* 扩展配置
*/
Extra extra;
/**
* FFmpeg 路径相关配置
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.ffmpeg")
@ConfigurationProperties(prefix = "env.ffmpeg")
public static class FFmpeg implements Serializable {
@Serial
private static final long serialVersionUID = 972365906869473179L;
/**
* ffmpeg 路径
*/
String path;
}
/**
* 扩展环境配置变量
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.extra")
@ConfigurationProperties(prefix = "env.extra")
public static class Extra implements Serializable {
@Serial
private static final long serialVersionUID = -521508901960998020L;
/**
* 公共用户名
*/
String username;
/**
* 公共密码
*/
String password;
}
} }
...@@ -4,7 +4,7 @@ package com.yiring.common.config; ...@@ -4,7 +4,7 @@ package com.yiring.common.config;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import org.n52.jackson.datatype.jts.JtsModule; import org.n52.jackson.datatype.jts.JtsModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration; ...@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
@RequiredArgsConstructor
public class JacksonConfig { public class JacksonConfig {
@Resource final JavaTimeModule javaTimeModule;
JavaTimeModule javaTimeModule;
@Bean @Bean
public ObjectMapper objectMapper() { public ObjectMapper objectMapper() {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.core; package com.yiring.common.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.PropertyKey;
/** /**
* 标准的响应对象(所有的接口响应内容格式都应该是一致的) * 标准的响应对象(所有的接口响应内容格式都应该是一致的)
...@@ -21,63 +23,70 @@ import lombok.extern.slf4j.Slf4j; ...@@ -21,63 +23,70 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@SuppressWarnings({ "unchecked", "unused" }) @SuppressWarnings({ "unchecked", "unused" })
@ApiModel("Result")
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
@Slf4j
@Data @Data
@Builder @Builder
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class Result<T extends Serializable> implements Serializable { public class Result<T> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -4802543396830024571L; private static final long serialVersionUID = -4802543396830024571L;
/** /**
* 注入 I18n
*/
protected static final I18n i18n;
static {
i18n = SpringUtil.getBean(I18n.class);
}
/**
* 接口响应时间 * 接口响应时间
*/ */
@ApiModelProperty(value = "响应时间", example = "2021-01-01 00:00:00") @Schema(description = "响应时间", example = "2021-01-01 00:00:00")
String timestamp; String timestamp;
/** /**
* 接口耗时(单位:秒)通常在调试阶段出现 * 接口耗时(单位:秒)通常在调试阶段出现
*/ */
@ApiModelProperty(value = "耗时", example = "0.001s") @Schema(description = "耗时", example = "0.001s")
String times; String times;
/** /**
* 响应状态码 * 响应状态码
*/ */
@ApiModelProperty(value = "状态码", example = "200") @Schema(description = "状态码", example = "200", defaultValue = "200")
Integer status; Integer status;
/** /**
* 业务标识码 * 响应消息
*/ */
@ApiModelProperty(value = "业务标识码", example = "0") @Schema(description = "消息", example = "OK", defaultValue = "OK")
Integer code; String message;
/** /**
* 响应消息 * 业务标识码
*/ */
@ApiModelProperty(value = "消息", example = "OK") @Schema(description = "业务标识码", example = "0", nullable = true)
String message; Integer code;
/** /**
* 详细信息,通常为参数校验结果或自定义消息 * 详细信息,通常为参数校验结果或自定义消息
*/ */
@ApiModelProperty(value = "详细信息", example = "Details message") @Schema(description = "详细信息", nullable = true)
String details; String details;
/** /**
* 异常信息,通常在出现服务器错误时会出现该异常 * 异常信息,通常在出现服务器错误时会出现该异常
*/ */
@ApiModelProperty(value = "异常信息", notes = "出现错误时会出现该字段", example = "Error message") @Schema(description = "异常信息", nullable = true)
String error; String error;
/** /**
* 响应内容 * 响应内容
*/ */
@ApiModelProperty("内容") @Schema(description = "内容")
T body; T body;
/** /**
...@@ -86,8 +95,24 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -86,8 +95,24 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see com.yiring.common.core.Status * @see com.yiring.common.core.Status
*/ */
public static <T extends Serializable> Result<T> ok() { public static <T> Result<T> ok() {
return (Result<T>) Result.builder().status(Status.OK.value()).message(Status.OK.getReasonPhrase()).build(); return (Result<T>) Result.builder().status(Status.OK.value()).message(t(Status.OK.getReasonPhrase())).build();
}
/**
* 返回成功响应内容
*
* @param body {@link String} {@link I18n}
* @return Result
* @see com.yiring.common.core.Status
*/
public static <T> Result<T> ok(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String body) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
.message(t(Status.OK.getReasonPhrase()))
.body(t(body))
.build();
} }
/** /**
...@@ -96,22 +121,42 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -96,22 +121,42 @@ public class Result<T extends Serializable> implements Serializable {
* @param body {@link Object} * @param body {@link Object}
* @return Result * @return Result
*/ */
public static <T extends Serializable> Result<T> ok(T body) { public static <T> Result<T> ok(T body) {
return (Result<T>) Result return (Result<T>) Result
.builder() .builder()
.status(Status.OK.value()) .status(Status.OK.value())
.message(Status.OK.getReasonPhrase()) .message(t(Status.OK.getReasonPhrase()))
.body(body) .body(body)
.build(); .build();
} }
/** /**
* 返回默认的 400 错误响应
*
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T> Result<T> no() {
return no(Status.BAD_REQUEST);
}
/**
* 返回默认的 400 错误响应
*
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T> Result<T> no(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(Status.BAD_REQUEST, details);
}
/**
* 返回失败响应内容 * 返回失败响应内容
* *
* @return Result * @return Result
* @see Status#BAD_REQUEST * @see Status#BAD_REQUEST
*/ */
public static <T extends Serializable> Result<T> no(Status status) { public static <T> Result<T> no(Status status) {
return no(status, null, null, null); return no(status, null, null, null);
} }
...@@ -121,7 +166,7 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -121,7 +166,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, String details) { public static <T> Result<T> no(Status status, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(status, null, details, null); return no(status, null, details, null);
} }
...@@ -131,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -131,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, Throwable error) { public static <T> Result<T> no(Status status, Throwable error) {
return no(status, null, null, error); return no(status, null, null, error);
} }
...@@ -141,13 +186,28 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -141,13 +186,28 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result * @return Result
* @see Status * @see Status
*/ */
public static <T extends Serializable> Result<T> no(Status status, Integer code, String details, Throwable error) { public static <T> Result<T> no(
Status status,
Integer code,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error
) {
if (Objects.isNull(code) && Objects.nonNull(details)) {
String prefix = "Code.";
if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, "");
code = Convert.toInt(codeText);
} else {
code = -1;
}
}
Result<T> result = (Result<T>) Result Result<T> result = (Result<T>) Result
.builder() .builder()
.status(status.value()) .status(status.value())
.message(status.getReasonPhrase()) .message(t(status.getReasonPhrase()))
.code(code) .code(code)
.details(details) .details(t(details))
.build(); .build();
if (error != null) { if (error != null) {
...@@ -156,4 +216,18 @@ public class Result<T extends Serializable> implements Serializable { ...@@ -156,4 +216,18 @@ public class Result<T extends Serializable> implements Serializable {
return result; return result;
} }
/**
* i18n 默认值获取简单包装
*
* @param message 文本消息
* @return i18n 翻译结果文本消息
*/
public static String t(String message) {
if (message == null) {
return null;
}
return i18n.get(message, message);
}
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.core; package com.yiring.common.core;
import com.yiring.common.exception.FailStatusException;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* API 响应状态码 * API 响应状态码
* 包含系统和业务两个维度 * 包含系统和业务两个维度
*
* @author ifzm
*/ */
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
...@@ -13,73 +17,73 @@ public enum Status { ...@@ -13,73 +17,73 @@ public enum Status {
/** /**
* 成功 * 成功
*/ */
OK(200, "OK"), OK(200, "Status.OK"),
/** /**
* 用户认证失败 * 用户认证失败
*/ */
NON_AUTHORITATIVE_INFORMATION(203, "认证失败"), NON_AUTHORITATIVE_INFORMATION(203, "Status.NON_AUTHORITATIVE_INFORMATION"),
/** /**
* 失败的请求,通常是一些验证错误 * 失败的请求,通常是一些验证错误
*/ */
BAD_REQUEST(400, "FAIL"), BAD_REQUEST(400, "Status.BAD_REQUEST"),
/** /**
* 鉴权失败 * 鉴权失败
*/ */
UNAUTHORIZED(401, "凭证过期"), UNAUTHORIZED(401, "Status.UNAUTHORIZED"),
/** /**
* Token 错误/失效 * 禁止访问(可能是二级认证失败)
*/ */
FORBIDDEN(403, "禁止访问"), FORBIDDEN(403, "Status.FORBIDDEN"),
/** /**
* 找不到资源 * 找不到资源
*/ */
NOT_FOUND(404, "Not Found"), NOT_FOUND(404, "Status.NOT_FOUND"),
/** /**
* 不支持的请求类型 * 不支持的请求类型
*/ */
METHOD_NOT_ALLOWED(405, "不支持的请求类型"), METHOD_NOT_ALLOWED(405, "Status.METHOD_NOT_ALLOWED"),
/** /**
* 参数校验失败 * 参数校验失败
*/ */
EXPECTATION_FAILED(417, "无效参数"), EXPECTATION_FAILED(417, "Status.EXPECTATION_FAILED"),
/** /**
* 服务器错误 * 服务器错误
*/ */
INTERNAL_SERVER_ERROR(500, "服务器错误"), INTERNAL_SERVER_ERROR(500, "Status.INTERNAL_SERVER_ERROR"),
/** /**
* 未知错误 * 未知错误
*/ */
UNKNOWN_ERROR(500, "未知错误"), UNKNOWN_ERROR(500, "Status.UNKNOWN_ERROR"),
/** /**
* API 未实现 * API 未实现
*/ */
NOT_IMPLEMENTED(501, "API 未实现"), NOT_IMPLEMENTED(501, "Status.NOT_IMPLEMENTED"),
/** /**
* 服务异常(网关提醒) * 服务异常(网关提醒)
*/ */
BAD_GATEWAY(502, "Bad Gateway"), BAD_GATEWAY(502, "Status.BAD_GATEWAY"),
/** /**
* 服务暂停(网关提醒) * 服务暂停(网关提醒)
*/ */
SERVICE_UNAVAILABLE(503, "Service Unavailable"); SERVICE_UNAVAILABLE(503, "Status.SERVICE_UNAVAILABLE");
private final int value; private final int value;
private final String reasonPhrase; private final String reasonPhrase;
Status(int value, String reasonPhrase) { Status(int value, @PropertyKey(resourceBundle = "i18n.status") String reasonPhrase) {
this.value = value; this.value = value;
this.reasonPhrase = reasonPhrase; this.reasonPhrase = reasonPhrase;
} }
...@@ -137,4 +141,29 @@ public enum Status { ...@@ -137,4 +141,29 @@ public enum Status {
public String toString() { public String toString() {
return this.value + " " + name(); return this.value + " " + name();
} }
/**
* 快速失败异常
*/
public FailStatusException exception() {
return exception(null);
}
/**
* 快速失败异常
*
* @param message 异常消息
*/
public FailStatusException exception(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
return new FailStatusException(this, message);
}
/**
* 暴露异常
*
* @param message 异常消息
*/
public void expose(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) throws FailStatusException {
throw exception(message);
}
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.domain; package com.yiring.common.domain;
import com.yiring.common.snowflake.SnowflakeId;
import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
...@@ -14,7 +12,7 @@ import org.hibernate.annotations.Comment; ...@@ -14,7 +12,7 @@ import org.hibernate.annotations.Comment;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.snowflake.SnowflakeId; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/** /**
* 基础表抽象类 * 基础表抽象类
...@@ -33,6 +31,7 @@ import org.hibernate.snowflake.SnowflakeId; ...@@ -33,6 +31,7 @@ import org.hibernate.snowflake.SnowflakeId;
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@SuperBuilder(toBuilder = true) @SuperBuilder(toBuilder = true)
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BasicEntity { public abstract class BasicEntity {
@Comment("主键") @Comment("主键")
...@@ -50,4 +49,13 @@ public abstract class BasicEntity { ...@@ -50,4 +49,13 @@ public abstract class BasicEntity {
@Column(nullable = false) @Column(nullable = false)
@UpdateTimestamp @UpdateTimestamp
LocalDateTime updateTime; LocalDateTime updateTime;
@Comment("删除时间")
LocalDateTime deleteTime;
public interface Where {
String EXIST = " delete_time is null ";
String DELETE_SET = " set delete_time = now() where id = ? ";
}
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.exception;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Status;
import java.io.Serial;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.PropertyKey;
/**
* 业务状态异常
*
* @author Jim
* @version 0.1
* 2022/3/28 11:36
*/
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BusinessException extends RuntimeException {
@Serial
private static final long serialVersionUID = -4226669531686389671L;
/**
* 状态码
*/
Status status;
/**
* 业务状态异常消息
*/
String message;
public BusinessException(Status status, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
this.status = status;
this.message = message;
}
public static BusinessException i18n(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
return new BusinessException(Status.BAD_REQUEST, message);
}
public static BusinessException i18n(
@NonNull Status status,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message
) {
return new BusinessException(status, message);
}
}
...@@ -30,4 +30,9 @@ public class FailStatusException extends RuntimeException { ...@@ -30,4 +30,9 @@ public class FailStatusException extends RuntimeException {
* 状态 * 状态
*/ */
Status status; Status status;
/**
* 异常消息
*/
String message;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "IdParam", description = "公共的 ID 查询参数") @Schema(name = "IdParam", description = "公共的 ID 查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -29,7 +27,7 @@ public class IdParam implements Serializable { ...@@ -29,7 +27,7 @@ public class IdParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "id", example = "1", required = true) @Parameter(description = "id", example = "1")
@NotNull(message = "id 不能为空") @NotBlank
String id; String id;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Collection;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -20,8 +19,7 @@ import lombok.experimental.FieldDefaults; ...@@ -20,8 +19,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel("IdsParam") @Schema(name = "IdsParam")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -32,15 +30,16 @@ public class IdsParam implements Serializable { ...@@ -32,15 +30,16 @@ public class IdsParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8379896695668632733L; private static final long serialVersionUID = -8379896695668632733L;
@ApiModelProperty(value = "ids 多个以逗号分割", example = "1,2", required = true) @Parameter(description = "ids 多个以逗号分割", example = "1,2")
@NotEmpty(message = "ids 不能为空") @NotBlank
String ids; String ids;
/** /**
* 获取 String 类型的 ID 集合 * 获取 String 类型的 ID 集合
*
* @return ID 集合 * @return ID 集合
*/ */
public Set<String> toIds() { public Collection<Serializable> toIds() {
return Arrays.stream(this.ids.split(",")).collect(Collectors.toSet()); return Arrays.stream(this.ids.split(",")).collect(Collectors.toSet());
} }
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -16,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2022/4/27 08:53 * 2022/4/27 08:53
*/ */
@ApiModel(value = "KeywordParam", description = "公共的关键字查询参数") @Schema(name = "KeywordParam", description = "公共的关键字查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -28,6 +27,7 @@ public class KeywordParam implements Serializable { ...@@ -28,6 +27,7 @@ public class KeywordParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456894L; private static final long serialVersionUID = -8690942241103456894L;
@ApiModelProperty(value = "关键字", example = "hi") @Parameter(description = "关键字", example = "hi")
@NotBlank
String keyword; String keyword;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
/** /**
* 公共的分页排序查询参数 * 公共的分页排序查询参数
* eg: 支持使用 @Validated(Group.Optional.class) 来忽略校验
* *
* @author ifzm * @author ifzm
* @version 0.1 2019/3/10 16:29 * @version 0.1 2019/3/10 16:29
*/ */
@ApiModel(value = "PageParam", description = "公共的分页排序查询参数") @Schema(name = "PageParam", description = "公共的分页排序查询参数")
@Data @Data
@SuperBuilder @SuperBuilder
@NoArgsConstructor @NoArgsConstructor
...@@ -35,20 +36,20 @@ public class PageParam implements Serializable { ...@@ -35,20 +36,20 @@ public class PageParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6103761701912769946L; private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10", required = true) @Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull(message = "分页条数不能为空") @NotNull
@DecimalMin(value = "1", message = "分页条数不能小于1") @Range(min = 1, max = 100)
Integer pageSize; Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1", required = true) @Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull(message = "当前页数不能为空") @NotNull
@DecimalMin(value = "1", message = "当前页数不能小于1") @Min(1)
Integer pageNo; Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id") @Schema(description = "排序字段", defaultValue = "id", example = "id")
String sortField; String sortField;
@ApiModelProperty(value = "排序方向(ASC|DESC)", example = "DESC") @Schema(description = "排序方向(ASC|DESC)", defaultValue = "DESC", example = "DESC")
Sort.Direction sortOrder; Sort.Direction sortOrder;
/** /**
...@@ -59,14 +60,27 @@ public class PageParam implements Serializable { ...@@ -59,14 +60,27 @@ public class PageParam implements Serializable {
*/ */
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
public static Pageable toPageable(PageParam param) { public static Pageable toPageable(PageParam param) {
if (param == null) { if (param == null || Objects.isNull(param.getPageNo()) || Objects.isNull(param.getPageSize())) {
return Pageable.unpaged(); return Pageable.unpaged();
} }
return PageParam.toPageable(param.getSortField(), param.getSortOrder(), param.getPageSize(), param.getPageNo());
}
/**
* 根据参数构建分页对象
*
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param pageSize 分页大小
* @param pageNo 分页页码
* @return Pageable
*/
public static Pageable toPageable(String sortField, Sort.Direction sortOrder, Integer pageSize, Integer pageNo) {
Sort sort = Sort.unsorted(); Sort sort = Sort.unsorted();
if (Objects.nonNull(param.getSortField())) { if (Objects.nonNull(sortField)) {
sort = Sort.by(new Sort.Order(param.getSortOrder(), param.getSortField())); sort = Sort.by(new Sort.Order(sortOrder, sortField));
} }
return PageRequest.of(param.getPageNo() - 1, param.getPageSize(), sort); return PageRequest.of(pageNo - 1, pageSize, sort);
} }
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults; ...@@ -17,8 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2019/5/28 22:11 * 2019/5/28 22:11
*/ */
@ApiModel(value = "PidParam", description = "公共的父级 ID 查询参数") @Schema(name = "PidParam", description = "公共的父级 ID 查询参数")
@Valid
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -29,7 +27,7 @@ public class PidParam implements Serializable { ...@@ -29,7 +27,7 @@ public class PidParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -8690942241103456893L; private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "pid", example = "0", required = true) @Parameter(description = "pid", example = "0")
@NotNull(message = "pid 不能为空") @NotBlank
String pid; String pid;
} }
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 Long 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateLongId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextId();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 String 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateStringId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextIdStr();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import lombok.experimental.UtilityClass;
/**
* 雪花 ID 生成器常量
*
* @author ifzm
* @version 0.1
* 2020/8/10 15:35
*/
@SuppressWarnings("unused")
@UtilityClass
public class SnowflakeId {
public final String GENERATOR = "snowflake";
public class Strategy {
public static final String STRING = "com.yiring.common.snowflake.GenerateStringId";
public static final String LONG = "com.yiring.common.snowflake.GenerateLongId";
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 枚举参数校验
*
* @author Jim
* @version 0.1
* 2022/9/29 16:04
*/
@Documented
@Retention(value = RUNTIME)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Constraint(validatedBy = { EnumValueValidated.class })
public @interface EnumValue {
/**
* 是否需要(true:不能为空,false:可以为空)
*/
boolean isRequire() default false;
/**
* 字符串数组
*/
String[] strValues() default {};
/**
* int数组
*/
int[] intValues() default {};
/**
* 枚举类
*/
Class<?>[] enumClass() default {};
String message() default "所传参数不在允许的值范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 枚举参数校验器
*
* @author Jim
* @version 0.1
* 2022/9/29 16:06
*/
public class EnumValueValidated implements ConstraintValidator<EnumValue, Object> {
private boolean isRequire;
private Set<String> strValues;
private List<Integer> intValues;
@Override
public void initialize(EnumValue constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
strValues = Set.of(constraintAnnotation.strValues());
intValues = Arrays.stream(constraintAnnotation.intValues()).boxed().collect(Collectors.toList());
isRequire = constraintAnnotation.isRequire();
// 将枚举类的 name 转小写存入 strValues 里面,作为校验参数
Optional
.ofNullable(constraintAnnotation.enumClass())
.ifPresent(e ->
Arrays
.stream(e)
.forEach(c ->
Arrays.stream(c.getEnumConstants()).forEach(v -> strValues.add(v.toString().toLowerCase()))
)
);
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null && !isRequire) {
return true;
}
if (value instanceof String) {
return strValues.contains(value);
}
if (value instanceof Integer) {
return intValues.stream().anyMatch(e -> e.equals(value));
}
return false;
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation;
import com.yiring.common.util.Commons;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation;
import jakarta.validation.ValidatorFactory;
import java.util.Set;
import lombok.Cleanup;
import lombok.experimental.UtilityClass;
/**
* 校验工具类
*
* @author Jim
* @version 0.1
* 2022/10/24 17:11
*/
@SuppressWarnings("unused")
@UtilityClass
public class ValidateUtil {
/**
* 对象校验
*
* @param t 目标对象
* @param groups 分组
* @param <T> 目标类
* @throws ConstraintViolationException 校验异常(结合全局异常拦截,快速失败模式)
*/
public static <T> void validate(T t, Class<?>... groups) {
@Cleanup
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Set<ConstraintViolation<T>> constraintViolations = factory.getValidator().validate(t, groups);
if (Commons.notEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation.group;
import jakarta.validation.groups.Default;
/**
* validate group
* 预设一些全局公用的参数参数分组
*
* @author Jim
* @version 0.1
* 2022/11/10 11:46
*/
@SuppressWarnings("unused")
public interface Group {
/**
* 数据添加分组
*/
interface Add extends Default {}
/**
* 数据修改分组
*/
interface Edit extends Default {}
/**
* 数据查询分组
*/
interface Query extends Default {}
/**
* 通用的必填分组
*/
interface Required extends Default {}
/**
* 可选的分组
*/
interface Optional {}
}
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.vo; package com.yiring.common.vo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.*; import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
...@@ -16,7 +18,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +18,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1 * @version 0.1
* 2022/3/23 16:47 * 2022/3/23 16:47
*/ */
@ApiModel(value = "DataVo", description = "公共数据响应输出") @Schema(name = "DataVo", description = "公共数据响应输出")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
...@@ -26,17 +28,18 @@ public class DataVo<T extends Serializable> implements Serializable { ...@@ -26,17 +28,18 @@ public class DataVo<T extends Serializable> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 2472779197432240431L; private static final long serialVersionUID = 2472779197432240431L;
@ApiModelProperty(value = "数据") @Schema(description = "数据")
T data; T data;
/** /**
* 通常在带有时效性的数据查询时有用途(可选参数) * 通常在带有时效性的数据查询时有用途(可选参数)
*/ */
@ApiModelProperty(value = "数据最新时间") @Schema(description = "数据最新时间")
LocalDateTime latest; LocalDateTime latest;
/** /**
* 构建一个 DataVo * 构建一个 DataVo
*
* @param data 数据 * @param data 数据
* @return DataVo * @return DataVo
*/ */
...@@ -47,7 +50,8 @@ public class DataVo<T extends Serializable> implements Serializable { ...@@ -47,7 +50,8 @@ public class DataVo<T extends Serializable> implements Serializable {
/** /**
* 构建一个 DataVo * 构建一个 DataVo
* @param data 数据 *
* @param data 数据
* @param latest 数据最新时间 * @param latest 数据最新时间
* @return DataVo * @return DataVo
*/ */
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.vo; package com.yiring.common.vo;
import io.swagger.annotations.ApiModel; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.*; import lombok.*;
...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2022/3/24 17:29 * 2022/3/24 17:29
*/ */
@ApiModel(value = "KeyValueVo", description = "键值对响应输出") @Schema(name = "KeyValueVo", description = "键值对响应输出")
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
...@@ -27,15 +26,21 @@ public class KeyValueVo implements Serializable { ...@@ -27,15 +26,21 @@ public class KeyValueVo implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -5238793972067296346L; private static final long serialVersionUID = -5238793972067296346L;
@ApiModelProperty(value = "key", example = "key") @Schema(description = "key", example = "key")
String key; String key;
@ApiModelProperty(value = "value", example = "value") @Schema(description = "value", example = "value")
String value; String value;
/** /**
* 扩展字段,可用于文本输出 * 扩展字段,可用于文本输出
*/ */
@ApiModelProperty(value = "label", example = "label") @Schema(description = "label", example = "label")
String label; String label;
/**
* 扩展字段,可用于显示禁用状态
*/
@Schema(description = "是否启用", example = "true")
String enable;
} }
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.vo; package com.yiring.common.vo;
import io.swagger.annotations.ApiModel; import com.yiring.common.util.Commons;
import io.swagger.annotations.ApiModelProperty; import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List; import java.util.List;
import lombok.*; import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import org.springframework.data.domain.Page;
/** /**
* 分页查询响应公共类 * 分页查询响应公共类
...@@ -16,7 +21,7 @@ import lombok.experimental.FieldDefaults; ...@@ -16,7 +21,7 @@ import lombok.experimental.FieldDefaults;
* @author ifzm * @author ifzm
* @version 0.1 2019/3/10 16:29 * @version 0.1 2019/3/10 16:29
*/ */
@ApiModel(value = "PageVo", description = "公共分页查询响应输出") @Schema(name = "PageVo", description = "公共分页查询响应输出")
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
...@@ -26,21 +31,33 @@ public class PageVo<T extends Serializable> implements Serializable { ...@@ -26,21 +31,33 @@ public class PageVo<T extends Serializable> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6103761701912769946L; private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "数据", required = true) @Schema(description = "数据")
List<T> data; List<T> data;
@ApiModelProperty(value = "数据总数", example = "100", required = true) @Schema(description = "数据总数", example = "100")
Long total; Long total;
/** /**
* 通常在带有时效性的数据查询时有用途(可选参数) * 通常在带有时效性的数据查询时有用途(可选参数)
*/ */
@ApiModelProperty(value = "数据最新时间") @Schema(description = "数据最新时间")
LocalDateTime latest; LocalDateTime latest;
/** /**
* 构建一个 PageVo * 构建一个 PageVo
*
* @param data 数据 * @param data 数据
* @return PageVo
*/
@SuppressWarnings({ "unused" })
public static <R extends Serializable> PageVo<R> build(List<R> data) {
return build(data, data.size());
}
/**
* 构建一个 PageVo
*
* @param data 数据
* @param total 总数据量 * @param total 总数据量
* @return PageVo * @return PageVo
*/ */
...@@ -51,8 +68,9 @@ public class PageVo<T extends Serializable> implements Serializable { ...@@ -51,8 +68,9 @@ public class PageVo<T extends Serializable> implements Serializable {
/** /**
* 构建一个 PageVo * 构建一个 PageVo
* @param data 数据 *
* @param total 总数据量 * @param data 数据
* @param total 总数据量
* @param latest 数据最新时间 * @param latest 数据最新时间
* @return PageVo * @return PageVo
*/ */
...@@ -64,4 +82,27 @@ public class PageVo<T extends Serializable> implements Serializable { ...@@ -64,4 +82,27 @@ public class PageVo<T extends Serializable> implements Serializable {
vo.setLatest(latest); vo.setLatest(latest);
return vo; return vo;
} }
/**
* 提取 Page 查询结果并转换成 Vo
*
* @param page Page 查询结果
* @param type Vo 类
* @param <S> 数据实体表
* @param <T> Vo 表
* @return PageVo
*/
@SuppressWarnings({ "unused" })
public static <S, T extends Serializable> PageVo<T> toPageVo(Page<S> page, Class<T> type) {
List<T> data = Commons.transform(page.toList(), type);
return build(data, page.getTotalElements());
}
/**
* @return 空的分页查询结果
*/
@SuppressWarnings("unused")
public static <T extends Serializable> PageVo<T> empty() {
return PageVo.build(Collections.emptyList(), 0);
}
} }
dependencies { dependencies {
implementation project(":basic-common:core") implementation project(":basic-common:core")
implementation project(":basic-common:i18n")
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-validation'
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-core // hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.swagger; package com.yiring.common.swagger;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Console; import cn.hutool.core.lang.Console;
import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.NetUtil;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver; import com.yiring.common.core.I18n;
import com.yiring.common.core.Status; import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.annotations.Api; import io.swagger.v3.oas.models.OpenAPI;
import java.util.Arrays; import io.swagger.v3.oas.models.info.Contact;
import java.util.List; import io.swagger.v3.oas.models.info.Info;
import java.util.function.Predicate; import io.swagger.v3.oas.models.tags.Tag;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.DeleteMapping;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; import org.springframework.web.bind.annotation.PostMapping;
import springfox.documentation.RequestHandler; import org.springframework.web.bind.annotation.PutMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/** /**
* Swagger Config * Swagger Config
...@@ -42,9 +37,8 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; ...@@ -42,9 +37,8 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Slf4j @Slf4j
@Profile("!prod") @Profile("!prod")
@EnableSwagger2WebMvc
@Configuration @Configuration
@Import(BeanValidatorPluginsConfiguration.class) @RequiredArgsConstructor
public class SwaggerConfig implements CommandLineRunner { public class SwaggerConfig implements CommandLineRunner {
@Value("${spring.application.name}") @Value("${spring.application.name}")
...@@ -56,71 +50,123 @@ public class SwaggerConfig implements CommandLineRunner { ...@@ -56,71 +50,123 @@ public class SwaggerConfig implements CommandLineRunner {
@Value("${server.servlet.context-path}") @Value("${server.servlet.context-path}")
String path; String path;
@Resource final I18n i18n;
OpenApiExtensionResolver openApiExtensionResolver;
@Bean(name = "api.default") @Bean
public Docket api() { public OpenAPI openAPI() {
return api("@default", List.of(""), PathSelectors.any()); Info info = new Info()
.title("API Doc")
.description(applicationName)
.version("1.0")
.contact(new Contact().name("© YiRing").url("https://yiring.com").email("developer@yiring.com"));
return new OpenAPI().info(info).externalDocs(new ExternalDocumentation());
}
@Bean(name = "api.any")
public GroupedOpenApi any() {
return api("default", List.of("com.yiring"), List.of("/**"), Collections.emptyList());
} }
@Bean(name = "api.auth") @Bean(name = "api.auth")
public Docket auth() { public GroupedOpenApi auth() {
return api("Auth", List.of("com.yiring.auth.web.auth"), PathSelectors.any()); return api("① Auth", List.of("com.yiring.auth.web"), List.of("/**"), List.of("/sys/**"));
} }
@Bean(name = "api.common") @Bean(name = "api.common")
public Docket common() { public GroupedOpenApi common() {
return api("公共", List.of("com.yiring.common.web", "com.yiring.app.web.common"), PathSelectors.any()); return api(
"② 公共",
List.of("com.yiring.common.web", "com.yiring.app.web.common"),
List.of("/**"),
Collections.emptyList()
);
} }
@Bean(name = "api.example") @Bean(name = "api.manage")
public Docket example() { public GroupedOpenApi manage() {
return api("示例", List.of("com.yiring.app.web.example"), PathSelectors.any()); return api(
"③ 系统管理",
List.of("com.yiring.auth.web.sys", "com.yiring.dict.web"),
List.of("/**"),
Collections.emptyList()
);
} }
private Docket api(String group, List<String> basePackages, Predicate<String> paths) { @Bean(name = "api.example")
// 扫描多个包 public GroupedOpenApi example() {
Predicate<RequestHandler> predicate = basePackages return api("④ 示例", List.of("com.yiring.app.web.example"), List.of("/**"), Collections.emptyList());
.stream()
.map(RequestHandlerSelectors::basePackage)
.reduce(Predicate::or)
.orElse(RequestHandlerSelectors.none());
return new Docket(DocumentationType.SWAGGER_2)
.groupName(group)
.apiInfo(apiInfo())
.useDefaultResponseMessages(false)
.globalResponseMessage(RequestMethod.GET, buildGlobalResponseMessage())
.globalResponseMessage(RequestMethod.POST, buildGlobalResponseMessage())
.globalResponseMessage(RequestMethod.DELETE, buildGlobalResponseMessage())
.globalResponseMessage(RequestMethod.PUT, buildGlobalResponseMessage())
.select()
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class).and(predicate))
.paths(paths)
.build()
.extensions(openApiExtensionResolver.buildExtensions(group));
} }
private ApiInfo apiInfo() { @Bean
return new ApiInfoBuilder() public OpenApiCustomizer sortTagCustom() {
.title("API Doc") String order = "x-order";
.description(applicationName) return api -> {
.version("1.0") List<Tag> tags = api
.contact(new Contact("© YiRing", "https://yiring.com", "developer@yiring.com")) .getTags()
.build(); .stream()
.sorted(
Comparator.comparing(tag ->
Convert.toInt(
Optional.ofNullable(tag.getExtensions()).orElseGet(HashMap::new).get(order),
Integer.MAX_VALUE
)
)
)
.peek(tag -> {
Map<String, Object> extensions = tag.getExtensions();
if (Objects.nonNull(extensions) && extensions.containsKey(order)) {
extensions.put(order, Convert.toInt(extensions.get(order)));
}
})
.toList();
api.setTags(tags);
};
} }
/** /**
* 构建全局响应消息 * 自定义 OperationId 用于优化 Pont 接口文件生成
*
* @return 所有自定义状态码消息
*/ */
private List<ResponseMessage> buildGlobalResponseMessage() { @Bean
return Arrays public OperationCustomizer operationIdCustomizer() {
.stream(Status.values()) return (operation, handlerMethod) -> {
.map(status -> new ResponseMessageBuilder().code(status.value()).message(status.getReasonPhrase()).build()) Class<?> superClazz = handlerMethod.getBeanType().getSuperclass();
.collect(Collectors.toList()); if (Objects.nonNull(superClazz)) {
Annotation[] annotations = handlerMethod.getMethod().getAnnotations();
String methodType = "GET";
for (Annotation annotation : annotations) {
if (annotation instanceof PostMapping) {
methodType = "POST";
} else if (annotation instanceof PutMapping) {
methodType = "PUT";
} else if (annotation instanceof DeleteMapping) {
methodType = "DELETE";
}
}
String beanName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
operation.setOperationId(String.format("%sUsing%s_%s", methodName, methodType, beanName));
}
return operation;
};
}
private GroupedOpenApi api(
String group,
List<String> basePackages,
List<String> pathsToMatch,
List<String> pathsToExclude
) {
return GroupedOpenApi
.builder()
.group(group)
.packagesToScan(basePackages.toArray(new String[0]))
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(io.swagger.v3.oas.annotations.Operation.class))
.pathsToMatch(pathsToMatch.toArray(new String[0]))
.pathsToExclude(pathsToExclude.toArray(new String[0]))
.addOperationCustomizer(operationIdCustomizer())
.build();
} }
@Override @Override
...@@ -130,6 +176,6 @@ public class SwaggerConfig implements CommandLineRunner { ...@@ -130,6 +176,6 @@ public class SwaggerConfig implements CommandLineRunner {
.stream() .stream()
.map(host -> "> http://" + host + ":" + port + path + "/doc.html") .map(host -> "> http://" + host + ":" + port + path + "/doc.html")
.collect(Collectors.joining("\n\t\t")); .collect(Collectors.joining("\n\t\t"));
Console.log("\n\t📖 API Doc (Swagger2): \n\t\t{}\n", link); Console.log("\n\t📖 API Doc (OpenAPI3): \n\t\t{}\n", link);
} }
} }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.config;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
/**
* 国际化配置
*
* @author Jim
* @version 0.1
* 2022/8/17 10:33
*/
@Order
@Configuration
@RequiredArgsConstructor
public class I18nConfig {
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* 解决多模块 i18n/messages 重复文件的读取问题
* <a href="https://stackoverflow.com/questions/17661252/spring-multi-module-i18n-with-modules-extending-the-messagesource-contents?noredirect=1&lq=1">...</a>
*
* @return MessageSource
*/
@Bean
public MessageSource messageSource() {
SmReloadableResourceBundleMessageSource messageSource = new SmReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath*:i18n/messages", "classpath:i18n/status", "classpath:i18n/validation");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setDefaultLocale(DEFAULT_LOCALE);
return messageSource;
}
@Bean
public LocaleResolver localeResolver() {
// header accept-language
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(DEFAULT_LOCALE);
return resolver;
}
@Bean
public LocalValidatorFactoryBean getValidator() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(DEFAULT_LOCALE);
messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation");
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.config;
import java.io.IOException;
import java.util.Properties;
import lombok.NonNull;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* @author Jim
* @version 0.1
* 2023/1/20 18:00
*/
public class SmReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected @NonNull PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
if (filename.startsWith(PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) {
return refreshClassPathProperties(filename, propHolder);
} else {
return super.refreshProperties(filename, propHolder);
}
}
private PropertiesHolder refreshClassPathProperties(String filename, PropertiesHolder propHolder) {
Properties properties = new Properties();
long lastModified = -1;
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder);
properties.putAll(holder.getProperties());
if (lastModified < resource.lastModified()) lastModified = resource.lastModified();
}
} catch (IOException ignored) {}
return new PropertiesHolder(properties, lastModified);
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.core;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 国际化
*
* @author Jim
* @version 0.1
* 2022/8/17 10:40
*/
@Slf4j
@Order
@Component
@RequiredArgsConstructor
public class I18n {
public static final String RESOURCE_BUNDLE = "i18n.messages";
final MessageSource messageSource;
/**
* 根据 MessageSourceResolvable 获取 I18n 消息
*
* @param resolvable MessageSourceResolvable
* @return 消息内容
*/
@SuppressWarnings("unused")
public String get(MessageSourceResolvable resolvable) {
return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale());
}
/**
* 根据 code 和注入参数获取 I18n 消息
* eg:
* default.nonnull = {0}不能为空
* message.username.not-empty = 用户姓名不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty")
*
* @param key 消息标识
* @param args 注入参数
* @return 消息内容
*/
@SuppressWarnings("unused")
public String get(@PropertyKey(resourceBundle = RESOURCE_BUNDLE) String key, Object... args) {
return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
}
/**
* 根据 code 和注入参数获取 I18n 消息
* eg:
* default.nonnull = {0}不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty", "用户姓名不能为空")
*
* @param key 消息标识
* @param defaultMessage 默认消息
* @param args 注入参数
* @return 消息内容
*/
@SuppressWarnings("unused")
public String get(
@PropertyKey(resourceBundle = RESOURCE_BUNDLE) String key,
String defaultMessage,
Object... args
) {
return messageSource.getMessage(key, args, defaultMessage, LocaleContextHolder.getLocale());
}
}
Status.OK=OK
Status.NON_AUTHORITATIVE_INFORMATION=Non-Authoritative Information
Status.BAD_REQUEST=Bad Request
Status.UNAUTHORIZED=Unauthorized
Status.FORBIDDEN=Forbidden
Status.NOT_FOUND=Not Found
Status.METHOD_NOT_ALLOWED=Method Not Allowed
Status.EXPECTATION_FAILED=Expectation Failed
Status.INTERNAL_SERVER_ERROR=Internal Server Error
Status.UNKNOWN_ERROR=Unknown Error
Status.NOT_IMPLEMENTED=Not Implemented
Status.BAD_GATEWAY=Bad Gateway
Status.SERVICE_UNAVAILABLE=Service Unavailable
Status.OK=\u6210\u529F
Status.NON_AUTHORITATIVE_INFORMATION=\u8BA4\u8BC1\u5931\u8D25
Status.BAD_REQUEST=\u8BF7\u6C42\u5931\u8D25
Status.UNAUTHORIZED=\u51ED\u8BC1\u8FC7\u671F
Status.FORBIDDEN=\u7981\u6B62\u8BBF\u95EE
Status.NOT_FOUND=\u627E\u4E0D\u5230\u8D44\u6E90
Status.METHOD_NOT_ALLOWED=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u7C7B\u578B
Status.EXPECTATION_FAILED=\u65E0\u6548\u53C2\u6570
Status.INTERNAL_SERVER_ERROR=\u670D\u52A1\u5668\u9519\u8BEF
Status.UNKNOWN_ERROR=\u672A\u77E5\u9519\u8BEF
Status.NOT_IMPLEMENTED=API \u672A\u5B9E\u73B0
Status.BAD_GATEWAY=\u670D\u52A1\u5F02\u5E38
Status.SERVICE_UNAVAILABLE=\u670D\u52A1\u6682\u505C
dependencies { dependencies {
implementation project(":basic-common:core") implementation project(":basic-common:core")
implementation project(":basic-common:util")
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// swagger(knife4j) // swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}" implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// minio // minio
implementation "io.minio:minio:${minioVersion}" implementation "io.minio:minio:${minioVersion}"
......
...@@ -23,10 +23,25 @@ import org.springframework.context.annotation.Configuration; ...@@ -23,10 +23,25 @@ import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "minio") @ConfigurationProperties(prefix = "minio")
public class MinioConfig { public class MinioConfig {
/**
* 通常是内网地址
*/
String endpoint; String endpoint;
/**
* 账户/访问标识
*/
String accessKey; String accessKey;
/**
* 密码/认证密钥
*/
String secretKey; String secretKey;
/**
* 默认储存桶名称
*/
String bucket; String bucket;
/**
* 通常是和 endpoint 相对的外网地址
*/
String domain; String domain;
@Bean @Bean
......
...@@ -9,6 +9,13 @@ import java.io.File; ...@@ -9,6 +9,13 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import lombok.Cleanup;
import lombok.NonNull;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
...@@ -32,10 +39,40 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -32,10 +39,40 @@ public record Minio(MinioConfig config, MinioClient client) {
* 获取文件访问地址 * 获取文件访问地址
* *
* @param object 文件相对地址(含路径) * @param object 文件相对地址(含路径)
* @param bucket 存储桶名称
* @return URI * @return URI
*/ */
public String getURI(String object) { public String getURI(String object, String bucket) {
return String.join("/", config.getDomain(), object); return String.join("/", config.getDomain(), bucket, object);
}
/**
* 获取文件访问地址(默认桶)
*
* @param object 文件相对地址(含路径)
* @return URI
*/
public String getDefaultURI(String object) {
return String.join("/", config.getDomain(), config.getBucket(), object);
}
/**
* 构建文件上传地址
*
* @param name 名称
* @param suffix 后缀,可以是空字符串
* @param folder 前缀目录,支持多层级
* @return 文件 object 相对地址
*/
public String buildUploadPath(@NonNull String name, @NonNull String suffix, String... folder) {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/M/d"));
String prefix = "upload/" + date;
if (Objects.nonNull(folder) && folder.length > 0) {
prefix += "/" + String.join("/", folder);
}
return prefix + "/" + name + suffix;
} }
/** /**
...@@ -53,7 +90,7 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -53,7 +90,7 @@ public record Minio(MinioConfig config, MinioClient client) {
.bucket(config.getBucket()) .bucket(config.getBucket())
.stream(is, -1, PutObjectArgs.MIN_MULTIPART_SIZE) .stream(is, -1, PutObjectArgs.MIN_MULTIPART_SIZE)
.object(object) .object(object)
.contentType(contentType) .contentType(Optional.ofNullable(contentType).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.build(); .build();
return client.putObject(args); return client.putObject(args);
} }
...@@ -72,7 +109,9 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -72,7 +109,9 @@ public record Minio(MinioConfig config, MinioClient client) {
} }
Path path = file.toPath(); Path path = file.toPath();
return putObject(Files.newInputStream(path), Files.probeContentType(path), object); @Cleanup
InputStream inputStream = Files.newInputStream(path);
return putObject(inputStream, Files.probeContentType(path), object);
} }
/** /**
...@@ -97,8 +136,8 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -97,8 +136,8 @@ public record Minio(MinioConfig config, MinioClient client) {
* @param object 文件相对地址 * @param object 文件相对地址
* @throws Exception 异常 * @throws Exception 异常
*/ */
public void remove(String object) throws Exception { public void remove(String bucket, String object) throws Exception {
RemoveObjectArgs args = RemoveObjectArgs.builder().bucket(config.getBucket()).object(object).build(); RemoveObjectArgs args = RemoveObjectArgs.builder().bucket(bucket).object(object).build();
client.removeObject(args); client.removeObject(args);
} }
...@@ -109,8 +148,8 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -109,8 +148,8 @@ public record Minio(MinioConfig config, MinioClient client) {
* @return 文件流 * @return 文件流
* @throws Exception 异常 * @throws Exception 异常
*/ */
public GetObjectResponse getObject(String object) throws Exception { public GetObjectResponse getObject(String bucket, String object) throws Exception {
GetObjectArgs args = GetObjectArgs.builder().bucket(config.getBucket()).object(object).build(); GetObjectArgs args = GetObjectArgs.builder().bucket(bucket).object(object).build();
return client.getObject(args); return client.getObject(args);
} }
...@@ -121,8 +160,8 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -121,8 +160,8 @@ public record Minio(MinioConfig config, MinioClient client) {
* @return 文件信息 * @return 文件信息
* @throws Exception 异常 * @throws Exception 异常
*/ */
public StatObjectResponse objectStat(String object) throws Exception { public StatObjectResponse objectStat(String bucket, String object) throws Exception {
StatObjectArgs args = StatObjectArgs.builder().bucket(config.getBucket()).object(object).build(); StatObjectArgs args = StatObjectArgs.builder().bucket(bucket).object(object).build();
return client.statObject(args); return client.statObject(args);
} }
...@@ -132,8 +171,8 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -132,8 +171,8 @@ public record Minio(MinioConfig config, MinioClient client) {
* @param prefix 文件路径前缀 * @param prefix 文件路径前缀
* @return 文件/文件夹集合 * @return 文件/文件夹集合
*/ */
public Iterable<Result<Item>> listObjects(String prefix) { public Iterable<Result<Item>> listObjects(String bucket, String prefix) {
ListObjectsArgs args = ListObjectsArgs.builder().bucket(config.getBucket()).prefix(prefix).build(); ListObjectsArgs args = ListObjectsArgs.builder().bucket(bucket).prefix(prefix).build();
return client.listObjects(args); return client.listObjects(args);
} }
...@@ -144,16 +183,9 @@ public record Minio(MinioConfig config, MinioClient client) { ...@@ -144,16 +183,9 @@ public record Minio(MinioConfig config, MinioClient client) {
* @param target 目标文件 object * @param target 目标文件 object
* @throws Exception 异常 * @throws Exception 异常
*/ */
public void copyObject(String source, String target) throws Exception { public void copyObject(String bucket, String source, String target) throws Exception {
CopySource copySource = CopySource.builder().bucket(config.getBucket()).object(source).build(); CopySource copySource = CopySource.builder().bucket(bucket).object(source).build();
CopyObjectArgs args = CopyObjectArgs.builder().bucket(bucket).object(target).source(copySource).build();
CopyObjectArgs args = CopyObjectArgs
.builder()
.bucket(config.getBucket())
.object(target)
.source(copySource)
.build();
client.copyObject(args); client.copyObject(args);
} }
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* MinIO 文件下载请求参数(可补充实现对私有桶的文件下载)
*
* @author Jim
* @version 0.1
* 2022/7/5 15:29
*/
@Schema(name = "DownloadParam", description = "文件下载请求参数")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DownloadParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456899L;
@Schema(name = "bucket", example = "public")
@NotEmpty(message = "存储桶不能为空")
String bucket;
@Schema(name = "object", example = "cat.jpg")
@NotEmpty(message = "文件对象不能为空")
String object;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务
*
* @author Jim
* @version 0.1
* 2023/1/29 16:18
*/
public interface FileManageService {
/**
* 文件上传
*
* @param file 文件
* @return 文件地址
*/
String upload(MultipartFile file) throws Exception;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传媒体文件预处理服务
*
* @author Jim
* @version 0.1
* 2022/9/23 16:31
*/
public interface UploadProcessService {
/**
* 对文件进行预处理,例如:
* 图片:haha.png -> haha.89x120.png 记录图片宽高,宽 = 89px,高 = 120px
* PDF:haha.pdf -> haha.P12.pdf 记录 PDF 文件总页数,P12 即代表 PDF 总共 12 页,同时可通过 haha.P12.pdf.1.jpg... 按序号读取到每一页 PDF 对应的图片
* 音视频:haha.mp3/4 -> haha.T12.mp3/4 记录音视频的时长(秒),T12 即代表音视频时长为 12 秒
* 视频封面:haha.mp4 -> haha.mp4.jpg 截取视频封面
*
* @param object 上传文件存储地址
* @param file 上传的文件
* @return 预处理后的文件地址(可能对文件名追加了时长、页数、分辨率等标识)
*/
default String handle(String object, MultipartFile file) {
return object;
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import com.yiring.common.service.UploadProcessService;
import org.springframework.stereotype.Service;
/**
* @author Jim
* @version 0.1
* 2023/2/13 11:14
*/
@Service
public class DefaultUploadProcessServiceImpl implements UploadProcessService {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.core.Minio;
import com.yiring.common.service.FileManageService;
import com.yiring.common.service.UploadProcessService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务实现
*
* @author Jim
* @version 0.1
* 2023/1/29 16:22
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FileManageServiceImpl implements FileManageService {
final Minio minio;
final UploadProcessService service;
@Override
public String upload(MultipartFile file) throws Exception {
// 获取文件名信息
String filename = file.getOriginalFilename();
if (StrUtil.isBlank(filename)) {
throw new RuntimeException("上传文件名不能为空");
}
// 获取文件信息以及默认存储地址
String uuid = IdUtil.fastSimpleUUID();
String object = minio.buildUploadPath(filename, "", uuid);
// 预处理(默认不做任何处理,具体逻辑需自行在外部实现)
object = service.handle(object, file);
// 上传原文件(如果是转换成了 m3u8 hls 文件则不保存原文件)
String suffix = ".m3u8";
if (filename.endsWith(suffix) || !object.endsWith(suffix)) {
minio.putObject(file.getInputStream(), file.getContentType(), object);
}
return minio.getDefaultURI(object);
}
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.web; package com.yiring.common.web;
import cn.hutool.core.lang.Snowflake; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport; import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.core.Minio; import com.yiring.common.core.Minio;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import io.minio.ObjectWriteResponse; import com.yiring.common.param.DownloadParam;
import io.swagger.annotations.Api; import com.yiring.common.service.FileManageService;
import io.swagger.annotations.ApiOperation; import com.yiring.common.util.FileUtils;
import io.swagger.annotations.ApiParam; import com.yiring.common.vo.ImageInfo;
import java.time.LocalDateTime; import io.minio.GetObjectResponse;
import java.time.format.DateTimeFormatter; import io.minio.StatObjectResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
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.HttpServletResponse;
import jakarta.validation.constraints.NotBlank;
import java.sql.Timestamp;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders; import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
/** /**
* Minio S3 * Minio S3
*
* @author Jim * @author Jim
*/ */
@Slf4j @Slf4j
@Validated @Validated
@SuppressWarnings({ "deprecation" }) @Tag(
@ApiSupport(order = -98) name = "文件管理",
@Api(tags = "文件管理", description = "file") description = "file",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9997") }) }
)
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/common/file/") @RequestMapping("/common/file/")
public class MinioController { public class MinioController {
final Minio minio; final Minio minio;
final FileManageService fileManageService;
/** /**
* minio 上传文件,成功返回文件 url * minio 上传文件,成功返回文件 url
*/ */
@ApiOperation(value = "文件上传", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation(summary = "文件上传")
@PostMapping(value = "upload", headers = HttpHeaders.CONTENT_TYPE + "=" + MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@ApiParam(value = "文件", required = true) @RequestPart("file") MultipartFile file) { public Result<String> upload(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) {
try { try {
Snowflake snowflake = IdUtil.getSnowflake(1, 1); String link = fileManageService.upload(file);
long uuid = snowflake.nextId(); return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
}
}
@Operation(summary = "Base64 图片上传")
@Parameter(
name = "base64Image",
description = "Base64 图片信息",
example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAALCAYAAABYpyyrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAATSURBVBhXYzAwNP6PjIeKgPF/ABj+RUX4hZfVAAAAAElFTkSuQmCC",
required = true,
in = ParameterIn.QUERY
)
@PostMapping(value = "uploadBase64Image")
public Result<String> uploadBase64Image(@NotBlank(message = "图片 Base64 信息不能为空") String base64Image) {
try {
// 解析 Base64 图片信息
ImageInfo image = FileUtils.parseBase64ImageText(base64Image);
// 获取文件信息以及默认存储地址
String uuid = IdUtil.getSnowflakeNextIdStr();
String object = minio.buildUploadPath(
uuid,
"." + image.getWidth() + "x" + image.getHeight() + "." + image.getSuffix()
);
// 文件上传 // 上传原文件
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/M/d")); minio.putObject(image.getStream(), image.getContentType(), object);
String folder = "upload/" + date + "/" + uuid; return Result.ok(minio.getDefaultURI(object));
ObjectWriteResponse response = minio.putObject(file, folder);
String uri = minio.getURI(response.object());
return Result.ok(uri);
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.no(Status.BAD_REQUEST, "上传失败"); throw Status.BAD_REQUEST.exception();
}
}
/**
* MinIO 文件下载(非公开桶)
*
* @param response HttpServletResponse
* @param param 请求参数
*/
@DownloadResponse
@Operation(summary = "文件下载")
@GetMapping(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void download(HttpServletResponse response, @ParameterObject @Validated DownloadParam param) {
try {
StatObjectResponse statObject = minio.objectStat(param.getBucket(), param.getObject());
GetObjectResponse object = minio.getObject(param.getBucket(), param.getObject());
long lastModified = Timestamp.from(statObject.lastModified().toInstant()).getTime();
FileUtils.download(
response,
object,
statObject.size(),
FileUtil.getName(statObject.object()),
statObject.contentType(),
lastModified
);
} catch (Exception e) {
throw Status.BAD_REQUEST.exception(e.getMessage());
} }
} }
} }
...@@ -3,7 +3,7 @@ package com.yiring.common.config; ...@@ -3,7 +3,7 @@ package com.yiring.common.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration; import java.time.Duration;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
...@@ -25,10 +25,10 @@ import org.springframework.data.redis.serializer.StringRedisSerializer; ...@@ -25,10 +25,10 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching @EnableCaching
@Configuration @Configuration
@RequiredArgsConstructor
public class RedisConfig { public class RedisConfig {
@Resource final ObjectMapper objectMapper;
ObjectMapper objectMapper;
@Bean @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
......
...@@ -3,7 +3,7 @@ package com.yiring.common.core; ...@@ -3,7 +3,7 @@ package com.yiring.common.core;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.Resource; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -18,10 +18,10 @@ import org.springframework.stereotype.Component; ...@@ -18,10 +18,10 @@ import org.springframework.stereotype.Component;
@SuppressWarnings({ "unused" }) @SuppressWarnings({ "unused" })
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor
public final class Redis { public final class Redis {
@Resource final RedisTemplate<String, Object> redisTemplate;
RedisTemplate<String, Object> redisTemplate;
/** /**
* 指定缓存失效时间 * 指定缓存失效时间
...@@ -83,7 +83,7 @@ public final class Redis { ...@@ -83,7 +83,7 @@ public final class Redis {
/** /**
* 普通缓存获取 * 普通缓存获取
* *
* @param key 键 * @param key
* @param type 类型 * @param type 类型
* @return 值 * @return 值
*/ */
...@@ -459,6 +459,7 @@ public final class Redis { ...@@ -459,6 +459,7 @@ public final class Redis {
/** /**
* 获取所有 key * 获取所有 key
*
* @param pattern 查询规则 * @param pattern 查询规则
* @return 所有匹配的 Key 集合 * @return 所有匹配的 Key 集合
*/ */
......
dependencies { dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// hutool // hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}" implementation "cn.hutool:hutool-core:${hutoolVersion}"
......
...@@ -20,4 +20,9 @@ public @interface Times { ...@@ -20,4 +20,9 @@ public @interface Times {
* 描述 * 描述
*/ */
String value() default ""; String value() default "";
/**
* 阈值(毫秒),当计算耗时超过阈值时,打印警告日志
*/
long threshold() default 0;
} }
...@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint; ...@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
...@@ -30,12 +31,22 @@ public class TimesAspect { ...@@ -30,12 +31,22 @@ public class TimesAspect {
@Around("times()") @Around("times()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
try { Object result = point.proceed();
return point.proceed(); long end = System.currentTimeMillis();
} finally { long times = end - start;
long end = System.currentTimeMillis();
log.info("[Times] {}: {} ms", getTimesValue(point), end - start); // 计算是否超过阈值,打印日志
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Times annotation = method.getAnnotation(Times.class);
String name = StrUtil.isEmpty(annotation.value()) ? method.getName() : annotation.value();
if (annotation.threshold() > 0 && times > annotation.threshold()) {
log.warn("[Times] {}: {} ms", name, times);
} else {
log.info("[Times] {}: {} ms", name, times);
} }
return result;
} }
public String getTimesValue(JoinPoint joinPoint) { public String getTimesValue(JoinPoint joinPoint) {
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.util; package com.yiring.common.util;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
import javax.servlet.http.HttpServletRequest;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
...@@ -21,7 +21,9 @@ import org.springframework.beans.BeanUtils; ...@@ -21,7 +21,9 @@ import org.springframework.beans.BeanUtils;
@UtilityClass @UtilityClass
public class Commons { public class Commons {
/** 代理 IP 请求头 */ /**
* 代理 IP 请求头
*/
private static final String[] HEADERS_TO_TRY = { private static final String[] HEADERS_TO_TRY = {
"X-Forwarded-For", "X-Forwarded-For",
"Proxy-Client-IP", "Proxy-Client-IP",
...@@ -69,8 +71,8 @@ public class Commons { ...@@ -69,8 +71,8 @@ public class Commons {
* @param collection 集合 * @param collection 集合
* @return 是否为空 * @return 是否为空
*/ */
public boolean isNullOrEmpty(Collection<?> collection) { public boolean notEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty(); return collection != null && !collection.isEmpty();
} }
/** /**
...@@ -85,11 +87,12 @@ public class Commons { ...@@ -85,11 +87,12 @@ public class Commons {
/** /**
* 对象 Copy * 对象 Copy
* @param source 源对象 *
* @param type 目标类型 * @param source 源对象
* @param type 目标类型
* @param ignoreProperties 忽略属性 * @param ignoreProperties 忽略属性
* @param <T> 目标类型
* @return 目标对象 * @return 目标对象
* @param <T> 目标类型
*/ */
public <T> T transform(Object source, Class<T> type, String... ignoreProperties) { public <T> T transform(Object source, Class<T> type, String... ignoreProperties) {
try { try {
...@@ -106,11 +109,12 @@ public class Commons { ...@@ -106,11 +109,12 @@ public class Commons {
/** /**
* 将集合通过 BeanUtils 反射转换成指定类型集合 * 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合 *
* @param type 目标类型 * @param list 原始数据集合
* @param type 目标类型
* @param ignoreProperties 忽略属性 * @param ignoreProperties 忽略属性
* @param <T> 目标类型集合 * @param <T> 目标类型集合
* @param <S> 原类型集合 * @param <S> 原类型集合
* @return 目标集合 * @return 目标集合
*/ */
public <T, S> List<T> transform(@NonNull List<S> list, Class<T> type, String... ignoreProperties) { public <T, S> List<T> transform(@NonNull List<S> list, Class<T> type, String... ignoreProperties) {
...@@ -119,12 +123,13 @@ public class Commons { ...@@ -119,12 +123,13 @@ public class Commons {
/** /**
* 将集合通过 BeanUtils 反射转换成指定类型集合 * 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合 *
* @param type 目标类型 * @param list 原始数据集合
* @param fn 自定义处理函数 * @param type 目标类型
* @param fn 自定义处理函数
* @param ignoreProperties 忽略属性 * @param ignoreProperties 忽略属性
* @param <T> 目标类型集合 * @param <T> 目标类型集合
* @param <S> 原类型集合 * @param <S> 原类型集合
* @return 目标集合 * @return 目标集合
*/ */
public <T, S> List<T> transform( public <T, S> List<T> transform(
...@@ -162,6 +167,7 @@ public class Commons { ...@@ -162,6 +167,7 @@ public class Commons {
public interface CallbackFunction<S, T> { public interface CallbackFunction<S, T> {
/** /**
* 执行方法 * 执行方法
*
* @param s 源对象 * @param s 源对象
* @param t 目标对象 * @param t 目标对象
*/ */
......
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.common.util; package com.yiring.common.util;
import cn.hutool.core.io.file.FileReader; import cn.hutool.core.codec.Base64;
import java.io.File; import cn.hutool.core.io.FileUtil;
import java.io.IOException; import com.yiring.common.vo.ImageInfo;
import jakarta.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import javax.servlet.http.HttpServletResponse; import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils;
/** /**
* 文件工具类 * 文件工具类
...@@ -24,15 +30,90 @@ public class FileUtils { ...@@ -24,15 +30,90 @@ public class FileUtils {
/** /**
* 文件下载 * 文件下载
*
* @param response HttpServletResponse * @param response HttpServletResponse
* @param file File * @param file File
* @throws IOException IOException
*/ */
public void download(HttpServletResponse response, File file) throws IOException { @SneakyThrows
public void download(HttpServletResponse response, File file) {
String filename = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8); String filename = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length())); long lastModified = basicFileAttributes.lastModifiedTime().toMillis();
response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(basicFileAttributes.size()));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
FileReader.create(file).writeToStream(response.getOutputStream(), true); response.setContentType(FileUtil.getMimeType(file.toPath()));
try (
FileInputStream object = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
}
/**
* 文件下载
*
* @param response HttpServletResponse
* @param object 文件流
* @param length 文件大小
* @param filename 文件名称(带后缀)
* @param contentType 文档类型
* @param lastModified 最后修改时间
*/
@SneakyThrows
public void download(
HttpServletResponse response,
InputStream object,
long length,
String filename,
String contentType,
long lastModified
) {
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(contentType);
try (
object;
BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
}
/**
* 解析 Base64 Image 文内容
*
* @param base64 图片 Base64 编码内容,含 data:image/xxx;base64, 前缀
* @return 图片信息
*/
@SneakyThrows
public ImageInfo parseBase64ImageText(String base64) {
if (!base64.startsWith("data:image")) {
throw new RuntimeException("Base64 Image 格式错误, 前缀应是 data:image");
}
// 解析数据
String type = base64.replaceAll("data:image/(.*);.*", "$1");
String contentType = "image/" + type;
String content = base64.substring(base64.indexOf(",") + 1);
byte[] bytes = Base64.decode(content);
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
// 构建结果
return ImageInfo
.builder()
.stream(new ByteArrayInputStream(bytes))
.image(image)
.width(image.getWidth())
.height(image.getHeight())
.suffix(type)
.size(bytes.length)
.contentType(contentType)
.build();
} }
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.vo;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Image 信息
*
* @author Jim
* @version 0.1
* 2022/8/15 10:02
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageInfo implements Serializable {
@Serial
private static final long serialVersionUID = 4042804283860857802L;
/**
*
*/
BufferedImage image;
/**
* IO
*/
InputStream stream;
/**
* 内容大小
*/
long size;
/**
* 类型,eg: image/png
*/
String contentType;
/**
* 后缀,eg: png
*/
String suffix;
/**
* 图片宽度
*/
int width;
/**
* 图片高度
*/
int height;
}
dependencies {
implementation project(':basic-common:core')
implementation project(':basic-common:util')
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-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.Category.DELETE_SQL;
import static com.yiring.dict.domain.Category.TABLE_NAME;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import java.io.Serial;
import java.io.Serializable;
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;
/**
* 分类字典
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@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),
@Index(columnList = Category.Fields.name),
@Index(columnList = Category.Fields.code),
}
)
@Comment("分类字典")
public class Category extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_CATEGORY";
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
public static final String DEFAULT_TOP_PID = "0";
@Serial
private static final long serialVersionUID = -3537807812313662319L;
@Comment("分类名称")
@Column(nullable = false)
String name;
@Comment("分类编号")
@Column(nullable = false, unique = true)
String code;
@Comment("分类描述")
String description;
@Comment("分类父级 ID")
String pid;
@Comment("分类是否启用")
@Column(nullable = false)
Boolean enable;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface CategoryRepository extends JpaRepository<Category, Serializable>, JpaSpecificationExecutor<Category> {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.Dict.DELETE_SQL;
import static com.yiring.dict.domain.Dict.TABLE_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
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;
/**
* 字典
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@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),
@Index(columnList = Dict.Fields.name),
@Index(columnList = Dict.Fields.code),
}
)
@Comment("数据字典")
public class Dict extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_DICT";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -6780729527484051504L;
@Comment("字典名称")
@Column(nullable = false)
String name;
@Comment("字典编号")
@Column(nullable = false, unique = true)
String code;
@Comment("字典描述")
String description;
@JsonIgnore
@Builder.Default
@Comment("字典选项集合")
@OneToMany(cascade = CascadeType.ALL, mappedBy = "dict")
List<DictItem> items = new ArrayList<>(0);
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.DictItem.DELETE_SQL;
import static com.yiring.dict.domain.DictItem.TABLE_NAME;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
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;
/**
* 字典选项
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@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),
@Index(columnList = DictItem.Fields.name),
@Index(columnList = DictItem.Fields.enable),
}
)
@Comment("数据字典选项")
public class DictItem extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_DICT_ITEM";
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -7321430621819435483L;
@Comment("字典")
@ManyToOne
@JoinColumn(nullable = false)
Dict dict;
@Comment("字典选项名称")
@Column(nullable = false)
String name;
@Comment("字典选项值")
@Column(nullable = false)
String value;
@Comment("字典选项描述")
String description;
@Comment("字典选项排序序号")
Integer serial;
@Comment("字典选项是否启用")
@Column(nullable = false)
Boolean enable;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface DictItemRepository extends JpaRepository<DictItem, Serializable>, JpaSpecificationExecutor<DictItem> {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface DictRepository extends JpaRepository<Dict, Serializable>, JpaSpecificationExecutor<Dict> {}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.param;
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.NotNull;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典选项参数
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictItemParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictItemParam implements Serializable {
@Serial
private static final long serialVersionUID = -4045504997838271449L;
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "字典 ID", example = "1")
@NotBlank
String dictId;
@Parameter(description = "字典选项名称", example = "男")
@NotBlank
String name;
@Parameter(description = "字典选项值", example = "1")
@NotBlank
String value;
@Parameter(description = "字典选项描述")
String description;
@Parameter(description = "字典选项排序序号")
Integer serial;
@Parameter(description = "字典选项是否启用", example = "true")
@NotNull
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.param;
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 java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典参数
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictParam implements Serializable {
@Serial
private static final long serialVersionUID = -5755754262686183535L;
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "字典名称", example = "性别")
@NotBlank
String name;
@Parameter(description = "字典编号", example = "GENDER")
@NotBlank
String code;
@Parameter(description = "字典描述")
String description;
}
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.app.exception; package com.yiring.dict.param;
import com.yiring.app.constant.Code; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
/** /**
* 业务状态异常 * 数据字典选项下拉查询参数
* *
* @author Jim * @author Jim
* @version 0.1 * @version 0.1
* 2022/3/28 11:36 * 2022/3/25 17:09
*/ */
@EqualsAndHashCode(callSuper = true) @Schema(name = "SelectorDictItemParam")
@Data @Data
@Builder
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class CodeException extends RuntimeException { public class SelectorDictItemParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -4226669531686389671L; private static final long serialVersionUID = -8165884102089982475L;
/** @Parameter(description = "字典 ID", example = "1")
* 业务状态 String dictId;
*/
Code code; @Parameter(description = "字典 ID", example = "1")
String dictCode;
@Parameter(description = "字典选项是否启用", example = "true")
Boolean enable;
} }
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典选项输出
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictItemVo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictItemVo implements Serializable {
@Serial
private static final long serialVersionUID = -5041312962732993815L;
@Schema(description = "id", example = "1")
String id;
@Schema(description = "字典选项名称", example = "男")
String name;
@Schema(description = "字典选项值", example = "1")
String value;
@Schema(description = "字典选项描述")
String description;
@Schema(description = "字典选项排序序号", example = "1")
Integer serial;
@Schema(description = "字典选项是否启用", example = "true")
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典输出
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictVo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictVo implements Serializable {
@Serial
private static final long serialVersionUID = -5041312962732993815L;
@Schema(description = "id", example = "1")
String id;
@Schema(description = "字典名称", example = "性别")
String name;
@Schema(description = "字典编号", example = "GENDER")
String code;
@Schema(description = "字典描述")
String description;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.web;
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.util.Commons;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.KeyValueVo;
import com.yiring.common.vo.PageVo;
import com.yiring.dict.domain.Dict;
import com.yiring.dict.domain.DictRepository;
import com.yiring.dict.param.DictParam;
import com.yiring.dict.vo.DictVo;
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.stream.Collectors;
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;
/**
* 数据字典管理
*
* @author Jim
* @version 0.1
* 2023/1/20 15:29
*/
@Slf4j
@Validated
@Tag(
name = "字典管理",
description = "Dict",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-96") }) }
)
@RestController
@RequestMapping("/sys/dict/")
@RequiredArgsConstructor
public class DictController {
final DictRepository dictRepository;
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) DictParam param) {
if (has(param.getCode())) {
throw BusinessException.i18n("Code.101000");
}
Dict entity = new Dict();
BeanUtils.copyProperties(param, entity);
dictRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) DictParam param) {
Optional<Dict> optional = dictRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
Dict entity = optional.get();
if (!entity.getCode().equals(param.getCode())) {
throw BusinessException.i18n("Code.101002");
}
BeanUtils.copyProperties(param, entity);
dictRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<Dict> dictList = dictRepository.findAllById(param.toIds());
dictRepository.deleteAll(dictList);
return Result.ok();
}
@Operation(summary = "查询")
@GetMapping("find")
public Result<DictVo> find(@ParameterObject @Validated IdParam param) {
Optional<Dict> optional = dictRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
Dict entity = optional.get();
DictVo vo = Commons.transform(entity, DictVo.class);
return Result.ok(vo);
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<DictVo>> page(@ParameterObject @Validated PageParam param) {
Page<Dict> page = dictRepository.findAll(PageParam.toPageable(param));
List<DictVo> data = Commons.transform(page.getContent(), DictVo.class);
PageVo<DictVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<KeyValueVo>> selector() {
List<Dict> dictList = dictRepository.findAll();
ArrayList<KeyValueVo> vos = dictList
.stream()
.map(dict -> KeyValueVo.builder().key(dict.getId()).value(dict.getCode()).label(dict.getName()).build())
.collect(Collectors.toCollection(ArrayList::new));
return Result.ok(vos);
}
/**
* 检查是否存在已有相同编码的字典
*
* @param code 字典编码
* @return 是否存在
*/
private boolean has(String code) {
Dict entity = Dict.builder().code(code).build();
return dictRepository.count(Example.of(entity)) > 0;
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.web;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.domain.BasicEntity;
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.util.Commons;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.KeyValueVo;
import com.yiring.common.vo.PageVo;
import com.yiring.dict.domain.Dict;
import com.yiring.dict.domain.DictItem;
import com.yiring.dict.domain.DictItemRepository;
import com.yiring.dict.param.DictItemParam;
import com.yiring.dict.param.SelectorDictItemParam;
import com.yiring.dict.vo.DictItemVo;
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.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
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
* 2023/1/20 15:29
*/
@Slf4j
@Validated
@Tag(
name = "字典选项管理",
description = "DictItem",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-95") }) }
)
@RestController
@RequestMapping("/sys/dict/item/")
@RequiredArgsConstructor
public class DictItemController {
final DictItemRepository dictItemRepository;
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) DictItemParam param) {
DictItem entity = new DictItem();
BeanUtils.copyProperties(param, entity);
entity.setDict(Dict.builder().id(param.getDictId()).build());
dictItemRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) DictItemParam param) {
Optional<DictItem> optional = dictItemRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
DictItem entity = optional.get();
BeanUtils.copyProperties(param, entity);
dictItemRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<DictItem> items = dictItemRepository.findAllById(param.toIds());
dictItemRepository.deleteAll(items);
return Result.ok();
}
@Operation(summary = "查询")
@GetMapping("find")
public Result<DictItemVo> find(@ParameterObject @Validated IdParam param) {
Optional<DictItem> optional = dictItemRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
DictItem entity = optional.get();
DictItemVo vo = Commons.transform(entity, DictItemVo.class);
return Result.ok(vo);
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<DictItemVo>> page(@ParameterObject @Validated PageParam param) {
Page<DictItem> page = dictItemRepository.findAll(PageParam.toPageable(param));
List<DictItemVo> data = Commons.transform(page.toList(), DictItemVo.class);
PageVo<DictItemVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<KeyValueVo>> selector(@ParameterObject @Validated SelectorDictItemParam param) {
if (StrUtil.isBlank(param.getDictId()) && StrUtil.isBlank(param.getDictCode())) {
throw BusinessException.i18n("Code.101001");
}
Specification<DictItem> specification = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (Objects.nonNull(param.getEnable())) {
predicates.add(cb.equal(root.get(DictItem.Fields.enable), param.getEnable()));
}
if (Objects.nonNull(param.getDictId())) {
predicates.add(cb.equal(root.get(DictItem.Fields.dict).get(BasicEntity.Fields.id), param.getDictId()));
}
if (Objects.nonNull(param.getDictCode())) {
predicates.add(cb.equal(root.get(DictItem.Fields.dict).get(Dict.Fields.code), param.getDictCode()));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
List<DictItem> items = dictItemRepository.findAll(specification);
ArrayList<KeyValueVo> vos = items
.stream()
.map(item -> KeyValueVo.builder().key(item.getId()).value(item.getValue()).label(item.getName()).build())
.collect(Collectors.toCollection(ArrayList::new));
return Result.ok(vos);
}
}
Code.101000=\u5B57\u5178\u7F16\u53F7\u91CD\u590D
Code.101002=\u5B57\u5178\u7F16\u53F7\u4E0D\u5141\u8BB8\u4FEE\u6539
Code.101001=id\u548Ccode\u53C2\u6570\u81F3\u5C11\u4F20\u9012\u4E00\u4E2A
Code.101000=\u5B57\u5178\u7F16\u53F7\u91CD\u590D
Code.101002=\u5B57\u5178\u7F16\u53F7\u4E0D\u5141\u8BB8\u4FEE\u6539
Code.101001=id\u548Ccode\u53C2\u6570\u81F3\u5C11\u4F20\u9012\u4E00\u4E2A
dependencies {
implementation project(':basic-auth')
implementation project(':basic-common:core')
implementation project(':basic-common:redis')
implementation "org.springframework.boot:spring-boot-starter-websocket"
implementation "org.springframework.boot:spring-boot-starter-reactor-netty"
// hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.config;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.yiring.common.core.Redis;
import com.yiring.websocket.constant.RedisKey;
import com.yiring.websocket.interceptor.ClientInboundChannelInterceptor;
import com.yiring.websocket.interceptor.ClientOutboundChannelInterceptor;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
/**
* WebSocketStompConfig
*
* @author ifzm
* @version 0.1
* 2019/9/25 20:12
*/
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
final Redis redis;
final ClientInboundChannelInterceptor clientInboundChannelInterceptor;
final ClientOutboundChannelInterceptor clientOutboundChannelInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/stomp/sock-js")
.setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS();
registry
.addEndpoint("/stomp/ws")
.setAllowedOriginPatterns("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
log.info("Init STOMP Endpoints Success.");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启动前先删除掉可能存在的残留STOMP连接缓存数据
redis.del(RedisKey.STOMP_ONLINE_USERS);
log.info("Clear STOMP online user info cache of redis.");
registry.setPreservePublishOrder(true);
registry.setUserDestinationPrefix("/user");
registry.setApplicationDestinationPrefixes("/app");
String stompPort = SpringUtil.getProperty("spring.rabbitmq.stomp-port");
if (Objects.isNull(stompPort)) {
// 1. 使用内存方式处理消息
registry.enableSimpleBroker("/topic", "/queue");
} else {
// 2. 使用 RabbitMQ 处理消息(需要安装 STOMP 插件)
RabbitProperties rabbitProperties = SpringUtil.getBean(RabbitProperties.class);
registry
.enableStompBrokerRelay("/topic", "/queue")
.setRelayPort(Convert.toInt(stompPort))
.setRelayHost(rabbitProperties.getHost())
.setVirtualHost(rabbitProperties.getVirtualHost())
.setClientLogin(rabbitProperties.getUsername())
.setClientPasscode(rabbitProperties.getPassword())
.setSystemLogin(rabbitProperties.getUsername())
.setSystemPasscode(rabbitProperties.getPassword());
}
log.info("Init RabbitMQ STOMP MessageBroker Success.");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(clientInboundChannelInterceptor);
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors(clientOutboundChannelInterceptor);
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.constant;
/**
* Redis Key 常量类
*
* @author fangzhimin
* 2018/9/4 15:51
*/
public interface RedisKey {
/**
* STOMP 在线用户关键数据
*/
String STOMP_ONLINE_USERS = "STOMP_ONLINE_USERS";
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.domain;
import com.alibaba.fastjson2.JSONObject;
import java.io.Serial;
import java.io.Serializable;
import java.security.Principal;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
/**
* StompPrincipal
*
* @author ifzm
* @version 0.1
* 2019/9/28 21:28
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
public class StompPrincipal implements Principal, Serializable {
@Serial
private static final long serialVersionUID = 5351052642945180737L;
Type type;
String user;
String session;
JSONObject options;
@Override
public String getName() {
return this.session;
}
public enum Type {
/**
* 游客用户
*/
GUEST_USER,
/**
* 登录用户
*/
LOGIN_USER,
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.interceptor;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.core.Redis;
import com.yiring.websocket.constant.RedisKey;
import com.yiring.websocket.domain.StompPrincipal;
import java.util.LinkedList;
import java.util.Map;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import org.springframework.stereotype.Component;
/**
* ClientInboundChannelInterceptor
* 接收客户端消息的拦截器
*
* @author ifzm
* @version 0.1
* 2019/9/28 20:58
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ClientInboundChannelInterceptor implements ChannelInterceptor {
final Redis redis;
final Auths auths;
private final Object lock = new Object();
@Override
public Message<?> preSend(@NonNull Message<?> message, @NonNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assert accessor != null;
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Object raw = message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS);
if (raw instanceof Map) {
StompPrincipal principal = new StompPrincipal();
principal.setSession(accessor.getSessionId());
Object tokens = ((Map<?, ?>) raw).get("token");
if (tokens instanceof LinkedList) {
String token = ((LinkedList<?>) tokens).getFirst().toString();
User user = auths.getUserByToken(token);
principal.setUser(user.getUsername());
principal.setType(StompPrincipal.Type.LOGIN_USER);
} else {
principal.setUser("Guest." + principal.getSession());
principal.setType(StompPrincipal.Type.GUEST_USER);
}
accessor.setUser(principal);
synchronized (lock) {
redis.hset(RedisKey.STOMP_ONLINE_USERS, principal.getSession(), principal);
log.info(
"STOMP Online Users: {} (incr: +1, session: {}, user: `{}`)",
redis.hsize(RedisKey.STOMP_ONLINE_USERS),
principal.getSession(),
principal.getUser()
);
}
}
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
StompPrincipal principal = (StompPrincipal) accessor.getUser();
if (principal != null && !message.getHeaders().containsKey(SimpMessageHeaderAccessor.HEART_BEAT_HEADER)) {
synchronized (lock) {
redis.hdel(RedisKey.STOMP_ONLINE_USERS, principal.getSession());
log.info(
"STOMP Online Users: {} (incr: -1, session: {}, user: `{}`)",
redis.hsize(RedisKey.STOMP_ONLINE_USERS),
principal.getSession(),
principal.getUser()
);
}
}
}
return message;
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.interceptor;
import com.alibaba.fastjson2.JSON;
import com.yiring.websocket.domain.StompPrincipal;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
/**
* ClientOutboundChannelInterceptor
* 向客户端输出消息的拦截器
*
* @author ifzm
* @version 0.1
* 2019/10/12 11:05
*/
@Slf4j
@Component
public class ClientOutboundChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(@NonNull Message<?> message, @NonNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
assert accessor != null;
if (StompCommand.CONNECTED.equals(accessor.getCommand())) {
StompPrincipal principal = (StompPrincipal) accessor.getUser();
return MessageBuilder.createMessage(JSON.toJSONBytes(principal), message.getHeaders());
}
return message;
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.registry;
import com.yiring.websocket.domain.StompPrincipal;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import lombok.NonNull;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
/**
* 自定义STOMP在线用户信息统计与操作
*
* @author ifzm
* @version 0.1
* 2019/10/10 21:19
*/
@Component
public class CustomStompUserRegistry implements StompUserRegistry, SmartApplicationListener {
/**
* sessionId, Principal
*/
private final Map<String, StompPrincipal> users = new ConcurrentHashMap<>();
private final Object lock = new Object();
@Override
public boolean supportsEventType(@NonNull Class<? extends ApplicationEvent> eventType) {
return AbstractSubProtocolEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(@NonNull ApplicationEvent event) {
AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;
Message<?> message = subProtocolEvent.getMessage();
SimpMessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(
message,
SimpMessageHeaderAccessor.class
);
Assert.state(accessor != null, "No SimpMessageHeaderAccessor");
String sessionId = accessor.getSessionId();
Assert.state(sessionId != null, "No session id");
if (event instanceof SessionConnectedEvent) {
Principal user = subProtocolEvent.getUser();
synchronized (lock) {
this.users.put(sessionId, (StompPrincipal) user);
}
} else if (event instanceof SessionDisconnectEvent) {
synchronized (lock) {
this.users.remove(sessionId);
}
}
}
@Override
public Set<StompPrincipal> getUsers() {
return new HashSet<>(this.users.values());
}
@Override
public int getUserCount() {
return this.users.size();
}
@Override
public StompPrincipal getUser(String sessionId) {
return this.users.get(sessionId);
}
@Override
public void updateUser(String sessionId, StompPrincipal principal) {
synchronized (lock) {
if (this.users.containsKey(sessionId)) {
this.users.put(sessionId, principal);
}
}
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.registry;
import com.yiring.websocket.domain.StompPrincipal;
import java.util.Set;
/**
* STOMP 用户注册器
*
* @author ifzm
* @version 0.1
* 2019/10/10 21:57
*/
public interface StompUserRegistry {
/**
* 获取所有在线的用户信息
*
* @return 用户信息集合
*/
@SuppressWarnings("unused")
Set<StompPrincipal> getUsers();
/**
* 获取所有在线用户的数量
*
* @return 在线用户的数量
*/
@SuppressWarnings("unused")
int getUserCount();
/**
* 根据SessionId获取用户信息
*
* @param sessionId sessionId
* @return StompPrincipal
*/
@SuppressWarnings("unused")
StompPrincipal getUser(String sessionId);
/**
* 更新用户信息
*
* @param sessionId sessionId
* @param principal StompPrincipal
*/
void updateUser(String sessionId, StompPrincipal principal);
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.websocket.web;
import cn.hutool.core.lang.UUID;
import com.alibaba.fastjson2.JSON;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.websocket.domain.StompPrincipal;
import com.yiring.websocket.registry.StompUserRegistry;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.simp.user.SimpUser;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.stereotype.Controller;
/**
* STOMP Receiver Controller
*
* @author ifzm
* @version 0.1
* 2019/9/28 23:13
*/
@Slf4j
@Controller
@RequiredArgsConstructor
public class StompReceiver {
final Auths auths;
final SimpMessagingTemplate simpMessagingTemplate;
final SimpUserRegistry simpUserRegistry;
final StompUserRegistry stompUserRegistry;
/**
* 登录
*
* @param accessor StompHeaderAccessor
*/
@MessageMapping("/login")
public void login(StompHeaderAccessor accessor, String token) {
try {
User user = auths.getUserByToken(token);
StompPrincipal principal = (StompPrincipal) accessor.getUser();
assert principal != null;
principal.setType(StompPrincipal.Type.LOGIN_USER);
principal.setUser(user.getUsername());
accessor.setUser(principal);
stompUserRegistry.updateUser(accessor.getSessionId(), principal);
log.info("STOMP user `{}` login success.", principal.getUser());
} catch (Exception e) {
simpMessagingTemplate.convertAndSendToUser(
Objects.requireNonNull(accessor.getSessionId()),
"/topic/notice",
Result.no(Status.UNAUTHORIZED)
);
}
}
/**
* 更新用户状态
*
* @param accessor 访问器
*/
@MessageMapping("/state")
public void state(StompHeaderAccessor accessor, String message) {
log.info("收到来自 STOMP Client `/app/state` 消息:{}", message);
StompPrincipal principal = (StompPrincipal) accessor.getUser();
assert principal != null;
principal.setOptions(JSON.parseObject(message));
accessor.setUser(principal);
log.info("principal info: {}", principal);
stompUserRegistry.updateUser(accessor.getSessionId(), principal);
}
@MessageMapping("/test")
public void test(StompHeaderAccessor accessor, String message) {
log.info("收到来自 STOMP Client `/app/test` 消息:{}", message);
Set<SimpUser> users = simpUserRegistry.getUsers();
log.info("{}", users);
simpMessagingTemplate.convertAndSendToUser(
Objects.requireNonNull(accessor.getSessionId()),
"/topic/reply",
Result.ok(UUID.fastUUID().toString(true))
);
}
}
plugins { plugins {
id 'java' id 'java'
// https://start.spring.io // https://start.spring.io
id 'org.springframework.boot' version '2.6.8' id 'org.springframework.boot' version '3.0.2'
id 'org.graalvm.buildtools.native' version '0.9.18'
// https://plugins.gradle.org/plugin/io.spring.dependency-management // https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'io.spring.dependency-management' version '1.1.0'
// https://plugins.gradle.org/plugin/com.diffplug.spotless // https://plugins.gradle.org/plugin/com.diffplug.spotless
id "com.diffplug.spotless" version "6.3.0" id "com.diffplug.spotless" version "6.13.0"
} }
ext { ext {
// Spotless // Spotless
// https://www.npmjs.com/package/prettier // https://www.npmjs.com/package/prettier
prettierVersion = '2.7.1' prettierVersion = '2.8.3'
// https://www.npmjs.com/package/prettier-plugin-java // https://www.npmjs.com/package/prettier-plugin-java
prettierJavaVersion = '1.6.2' prettierJavaVersion = '2.0.0'
// SpringCloud // SpringCloud
// https://start.spring.io/ // https://start.spring.io/
springCloudVersion = '2021.0.3' springCloudVersion = '2022.0.1'
// SpringBootAdmin
springBootAdminVersion = '3.0.0'
// Dependencies // Dependencies
// https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-boot-starter // // https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
knife4jVersion = '2.0.9' knife4jOpen3Version = '4.0.0'
// https://mvnrepository.com/artifact/io.swagger/swagger-annotations // https://mvnrepository.com/artifact/io.swagger/swagger-annotations
swaggerAnnotationsVersion = '1.6.6' swaggerAnnotationsVersion = '1.6.9'
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot-starter // https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter
saTokenVersion = '1.30.0' saTokenVersion = '1.34.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all // https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.4.M1' hutoolVersion = '5.8.12'
// https://mvnrepository.com/artifact/com.alibaba/fastjson // https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
fastJsonVersion = '2.0.7' fastJsonVersion = '2.0.23'
// https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core // https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core
xxlJobVersion = '2.3.1' xxlJobVersion = '2.3.1'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
okhttpVersion = '4.9.3' okhttpVersion = '4.10.0'
// https://mvnrepository.com/artifact/io.minio/minio // https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.4.2' minioVersion = '8.5.1'
// https://mvnrepository.com/artifact/com.vladmihalcea/hibernate-types-55 // https://mvnrepository.com/artifact/io.hypersistence/hypersistence-utils-hibernate-60
hibernateTypesVersion = '2.16.2' hibernateTypesVersion = '3.1.2'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial // https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial
hibernateSpatialVersion = '5.6.9.Final' hibernateSpatialVersion = '6.1.7.Final'
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core // https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
jtsVersion = '1.18.2' jtsVersion = '1.19.0'
// https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter
mybatisPlusVersion = '3.5.2'
// https://mvnrepository.com/artifact/com.github.liaochong/myexcel // https://mvnrepository.com/artifact/com.github.liaochong/myexcel
myexcelVersion = '4.2.0' myexcelVersion = '4.2.2'
// https://mvnrepository.com/artifact/org.jetbrains/annotations
jetbrainsAnnotationsVersion = '24.0.0'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
pdfboxVersion = '2.0.27'
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
ffmpegWrapperVersion = '0.7.0'
} }
allprojects { allprojects {
...@@ -53,6 +60,7 @@ allprojects { ...@@ -53,6 +60,7 @@ allprojects {
mavenLocal() mavenLocal()
// Nexus // Nexus
// maven { url 'http://10.111.102.83:8081/repository/aliyun-maven/' } // maven { url 'http://10.111.102.83:8081/repository/aliyun-maven/' }
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/public' }
mavenCentral() mavenCentral()
} }
...@@ -83,13 +91,16 @@ subprojects { ...@@ -83,13 +91,16 @@ subprojects {
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
} }
} }
[compileJava,compileTestJava,javadoc]*.options*.encoding ='UTF-8' [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) { tasks.withType(JavaCompile).tap {
options.encoding = 'UTF-8' configureEach {
options.encoding = 'UTF-8'
}
} }
bootJar { bootJar {
...@@ -130,7 +141,7 @@ subprojects { ...@@ -130,7 +141,7 @@ subprojects {
} }
} }
task hooks() { tasks.register('preCommit') {
// fix: CI/CD // fix: CI/CD
try { try {
// GitHook pre-commit (spotless, spotbugs) // GitHook pre-commit (spotless, spotbugs)
...@@ -143,12 +154,13 @@ task hooks() { ...@@ -143,12 +154,13 @@ task hooks() {
RESULT=\$? RESULT=\$?
exit \$RESULT exit \$RESULT
""" """
} catch (ignored) {} } catch (ignored) {
}
} }
gradle.getTaskGraph().whenReady { gradle.getTaskGraph().whenReady {
def skipHooks = gradle.startParameter.getSystemPropertiesArgs().containsKey('skip-hooks') def skipHooks = gradle.startParameter.getSystemPropertiesArgs().containsKey('skip-hooks')
if (!skipHooks) { if (!skipHooks) {
hooks preCommit
} }
} }
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')
const scopes = fs
.readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name.replace(/s$/, ''))
// precomputed scope
const scopeComplete = execSync('git status --porcelain || true')
.toString()
.trim()
.split('\n')
.find((r) => ~r.indexOf('M src'))
?.replace(/(\/)/g, '%%')
?.match(/src%%((\w|-)*)/)?.[1]
?.replace(/s$/, '')
/** @type {import('cz-git').UserConfig} */
module.exports = {
ignores: [(commit) => commit.includes('init')],
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'subject-case': [0],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
prompt: {
/** @use `yarn commit :f` */
alias: {
f: 'docs: fix typos',
r: 'docs: update README',
s: 'style: update code format',
b: 'build: bump dependencies',
c: 'chore: update config',
},
customScopesAlign: !scopeComplete ? 'top' : 'bottom',
defaultScope: scopeComplete,
scopes: [...scopes, 'mock'],
allowEmptyIssuePrefixs: false,
allowCustomIssuePrefixs: false,
// English
typesAppend: [
{ value: 'wip', name: 'wip: work in process' },
{ value: 'workflow', name: 'workflow: workflow improvements' },
{ value: 'types', name: 'types: type definition file changes' },
],
// 中英文对照版
// messages: {
// type: '选择你要提交的类型 :',
// scope: '选择一个提交范围 (可选):',
// customScope: '请输入自定义的提交范围 :',
// subject: '填写简短精炼的变更描述 :\n',
// body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
// breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
// footerPrefixsSelect: '选择关联issue前缀 (可选):',
// customFooterPrefixs: '输入自定义issue前缀 :',
// footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
// confirmCommit: '是否提交或修改commit ?',
// },
// types: [
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
// ],
// emptyScopesAlias: 'empty: 不填写',
// customScopesAlias: 'custom: 自定义',
},
}
...@@ -3,14 +3,13 @@ ...@@ -3,14 +3,13 @@
> IDEA > IDEA
<!-- prettier-ignore --> <!-- prettier-ignore -->
-[IDE Eval Rest](https://www.cnblogs.com/wang-cong/p/15150585.html) - IDEA 无限重置试用插件
-[GitHub Copilot](https://plugins.jetbrains.com/plugin/17718-github-copilot)
-[.ignore](https://plugins.jetbrains.com/plugin/7495--ignore) -[.ignore](https://plugins.jetbrains.com/plugin/7495--ignore)
-[GitToolBox](https://plugins.jetbrains.com/plugin/7499-gittoolbox) -[GitToolBox](https://plugins.jetbrains.com/plugin/7499-gittoolbox)
-[Grep Console](https://plugins.jetbrains.com/plugin/7125-grep-console) -[Grep Console](https://plugins.jetbrains.com/plugin/7125-grep-console)
-[Rainbow Brackets](https://plugins.jetbrains.com/plugin/10080-rainbow-brackets)
-[Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle) -[Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle)
-[Save Actions](https://plugins.jetbrains.com/plugin/7642-save-actions)
-[String Manipulation](https://plugins.jetbrains.com/plugin/2162-string-manipulation)
-[Resource Bundle Editor](https://plugins.jetbrains.com/plugin/17035-resource-bundle-editor)
- [Prettier](https://plugins.jetbrains.com/plugin/10456-prettier)
- [Nyan Progress Bar](https://plugins.jetbrains.com/plugin/8575-nyan-progress-bar) - [Nyan Progress Bar](https://plugins.jetbrains.com/plugin/8575-nyan-progress-bar)
- [JPA Buddy](https://plugins.jetbrains.com/plugin/15075-jpa-buddy)
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
### 基础 ### 基础
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [SpringBoot v2.6.x](https://spring.io/projects/spring-boot)
- [SpringBoot v3.0.x](https://spring.io/projects/spring-boot)
- [Lombok](https://projectlombok.org/) - [Lombok](https://projectlombok.org/)
- [Spring Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) - [Spring Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html)
- [Spring Data Jpa](https://spring.io/projects/spring-data-jpa) - [Spring Data Jpa](https://spring.io/projects/spring-data-jpa)
...@@ -12,33 +13,39 @@ ...@@ -12,33 +13,39 @@
### Tools ### Tools
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [Hutool](https://www.hutool.cn/) 工具包 - [Hutool](https://www.hutool.cn/) 工具包
- [FastJson](https://github.com/alibaba/fastjson) JSON 工具包 - [FastJson](https://github.com/alibaba/fastjson) JSON 工具包
### Doc ### Doc
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [Knife4j](https://doc.xiaominfo.com/) Swagger 增强包 - [Knife4j](https://doc.xiaominfo.com/) Swagger 增强包
### 扩展 ### 扩展
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [sa-token](https://sa-token.dev33.cn/) 权限认证框架 - [sa-token](https://sa-token.dev33.cn/) 权限认证框架
- [MinIO](https://docs.min.io/) 文件存储 - [MinIO](https://docs.min.io/) 文件存储
### 可选 ### 可选
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [MyBatis Plus](https://baomidou.com/) MyBatis 增强包(允许与 JPA 混合使用) - [MyBatis Plus](https://baomidou.com/) MyBatis 增强包(允许与 JPA 混合使用)
### 构建工具 ### 构建工具
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [Gradle](https://gradle.org/) - [Gradle](https://gradle.org/)
### 其他 ### 其他
<!-- prettier-ignore --> <!-- prettier-ignore -->
- [spotless](https://github.com/diffplug/spotless) 代码规范工具 - [spotless](https://github.com/diffplug/spotless) 代码规范工具
- [commitizen](https://github.com/commitizen) Git 提交规范工具 - [commitizen](https://github.com/commitizen) Git 提交规范工具
- ... - ...
# https://github.com/diffplug/spotless/issues/834 # https://github.com/diffplug/spotless/issues/834
# fix: jdk16+ # fix: jdk16+ (Repaired)
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ #org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ # --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ # --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ # --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED # --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
{ {
"name": "basic-api-project", "name": "basic-api-boot",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"log": "conventional-changelog -p angular -i CHANGELOG.md -s" "log": "conventional-changelog -p angular -i CHANGELOG.md -s"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.0",
"@commitlint/config-conventional": "^17.4.0",
"commitizen": "^4.2.6",
"conventional-changelog-cli": "^2.2.2",
"cz-conventional-changelog": "^3.3.0", "cz-conventional-changelog": "^3.3.0",
"conventional-changelog-cli": "^2.2.2" "cz-customizable": "^7.0.0",
"cz-git": "^1.4.1"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
"path": "./node_modules/cz-conventional-changelog" "path": "node_modules/cz-git"
} }
} }
} }
lockfileVersion: 5.3 lockfileVersion: 5.4
specifiers: specifiers:
'@commitlint/cli': ^17.4.0
'@commitlint/config-conventional': ^17.4.0
commitizen: ^4.2.6
conventional-changelog-cli: ^2.2.2 conventional-changelog-cli: ^2.2.2
cz-conventional-changelog: ^3.3.0 cz-conventional-changelog: ^3.3.0
cz-customizable: ^7.0.0
cz-git: ^1.4.1
devDependencies: devDependencies:
'@commitlint/cli': 17.4.1
'@commitlint/config-conventional': 17.4.0
commitizen: 4.2.6
conventional-changelog-cli: 2.2.2 conventional-changelog-cli: 2.2.2
cz-conventional-changelog: 3.3.0 cz-conventional-changelog: 3.3.0
cz-customizable: 7.0.0
cz-git: 1.4.1
packages: packages:
...@@ -31,6 +41,33 @@ packages: ...@@ -31,6 +41,33 @@ packages:
js-tokens: 4.0.0 js-tokens: 4.0.0
dev: true dev: true
/@commitlint/cli/17.4.1:
resolution: {integrity: sha512-W8OJwz+izY+fVwyUt1HveCDmABMZNRVZHSVPw/Bh9Y62tp11SmmQaycgbsYLMiMy7JGn4mAJqEGlSHS9Uti9ZQ==}
engines: {node: '>=v14'}
hasBin: true
dependencies:
'@commitlint/format': 17.4.0
'@commitlint/lint': 17.4.0
'@commitlint/load': 17.4.1
'@commitlint/read': 17.4.0
'@commitlint/types': 17.4.0
execa: 5.1.1
lodash.isfunction: 3.0.9
resolve-from: 5.0.0
resolve-global: 1.0.0
yargs: 17.6.2
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
dev: true
/@commitlint/config-conventional/17.4.0:
resolution: {integrity: sha512-G4XBf45J4ZMspO4NwBFzY3g/1Kb+B42BcIxeikF8wucQxcyxcmhRdjeQpRpS1XEcBq5pdtEEQFipuB9IuiNFhw==}
engines: {node: '>=v14'}
dependencies:
conventional-changelog-conventionalcommits: 5.0.0
dev: true
/@commitlint/config-validator/16.2.1: /@commitlint/config-validator/16.2.1:
resolution: {integrity: sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw==} resolution: {integrity: sha512-hogSe0WGg7CKmp4IfNbdNES3Rq3UEI4XRPB8JL4EPgo/ORq5nrGTVzxJh78omibNuB8Ho4501Czb1Er1MoDWpw==}
engines: {node: '>=v12'} engines: {node: '>=v12'}
...@@ -40,12 +77,63 @@ packages: ...@@ -40,12 +77,63 @@ packages:
dev: true dev: true
optional: true optional: true
/@commitlint/config-validator/17.4.0:
resolution: {integrity: sha512-Sa/+8KNpDXz4zT4bVbz2fpFjvgkPO6u2V2fP4TKgt6FjmOw2z3eEX859vtfeaTav/ukBw0/0jr+5ZTZp9zCBhA==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 17.4.0
ajv: 8.12.0
dev: true
/@commitlint/ensure/17.4.0:
resolution: {integrity: sha512-7oAxt25je0jeQ/E0O/M8L3ADb1Cvweu/5lc/kYF8g/kXatI0wxGE5La52onnAUAWeWlsuvBNar15WcrmDmr5Mw==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 17.4.0
lodash.camelcase: 4.3.0
lodash.kebabcase: 4.1.1
lodash.snakecase: 4.1.1
lodash.startcase: 4.4.0
lodash.upperfirst: 4.3.1
dev: true
/@commitlint/execute-rule/16.2.1: /@commitlint/execute-rule/16.2.1:
resolution: {integrity: sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g==} resolution: {integrity: sha512-oSls82fmUTLM6cl5V3epdVo4gHhbmBFvCvQGHBRdQ50H/690Uq1Dyd7hXMuKITCIdcnr9umyDkr8r5C6HZDF3g==}
engines: {node: '>=v12'} engines: {node: '>=v12'}
dev: true dev: true
optional: true optional: true
/@commitlint/execute-rule/17.4.0:
resolution: {integrity: sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==}
engines: {node: '>=v14'}
dev: true
/@commitlint/format/17.4.0:
resolution: {integrity: sha512-Z2bWAU5+f1YZh9W76c84J8iLIWIvvm+mzqogTz0Nsc1x6EHW0Z2gI38g5HAjB0r0I3ZjR15IDEJKhsxyblcyhA==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 17.4.0
chalk: 4.1.2
dev: true
/@commitlint/is-ignored/17.4.0:
resolution: {integrity: sha512-mkRuBlPUaBimvSvJyIHEHEW1/jP1SqEI7NOoaO9/eyJkMbsaiv5b1QgDYL4ZXlHdS64RMV7Y21MVVzuIceImDA==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 17.4.0
semver: 7.3.8
dev: true
/@commitlint/lint/17.4.0:
resolution: {integrity: sha512-HG2YT4TUbQKs9v8QvpQjJ6OK+fhflsDB8M+D5tLrY79hbQOWA9mDKdRkABsW/AAhpNI9+zeGUWF3jj245jSHKw==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/is-ignored': 17.4.0
'@commitlint/parse': 17.4.0
'@commitlint/rules': 17.4.0
'@commitlint/types': 17.4.0
dev: true
/@commitlint/load/16.2.3: /@commitlint/load/16.2.3:
resolution: {integrity: sha512-Hb4OUlMnBUK6UxJEZ/VJ5k0LocIS7PtEMbRXEAA7eSpOgORIFexC4K/RaRpVd5UTtu3M0ST3ddPPijF9rdW6nw==} resolution: {integrity: sha512-Hb4OUlMnBUK6UxJEZ/VJ5k0LocIS7PtEMbRXEAA7eSpOgORIFexC4K/RaRpVd5UTtu3M0ST3ddPPijF9rdW6nw==}
engines: {node: '>=v12'} engines: {node: '>=v12'}
...@@ -58,7 +146,7 @@ packages: ...@@ -58,7 +146,7 @@ packages:
'@types/node': 17.0.29 '@types/node': 17.0.29
chalk: 4.1.2 chalk: 4.1.2
cosmiconfig: 7.0.1 cosmiconfig: 7.0.1
cosmiconfig-typescript-loader: 1.0.9_5281fe59fc32158e106b8b5e2bebb315 cosmiconfig-typescript-loader: 1.0.9_kka74wp4giky4edlrnpcx25tcu
lodash: 4.17.21 lodash: 4.17.21
resolve-from: 5.0.0 resolve-from: 5.0.0
typescript: 4.6.3 typescript: 4.6.3
...@@ -68,6 +156,54 @@ packages: ...@@ -68,6 +156,54 @@ packages:
dev: true dev: true
optional: true optional: true
/@commitlint/load/17.4.1:
resolution: {integrity: sha512-6A7/LhIaQpL4ieciIDcVvK2d5z/UI1GBrtDaHm6sQSCL0265clB2/F7XKQNTJHXv9yG4LByT2r+QCpM4GugIfw==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/config-validator': 17.4.0
'@commitlint/execute-rule': 17.4.0
'@commitlint/resolve-extends': 17.4.0
'@commitlint/types': 17.4.0
'@types/node': 17.0.29
chalk: 4.1.2
cosmiconfig: 8.0.0
cosmiconfig-typescript-loader: 4.3.0_asoaktcvsaefgrgziobsocgewq
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
lodash.uniq: 4.5.0
resolve-from: 5.0.0
ts-node: 10.9.1_u3h3q7xqdg7nitfvwf64wuo7zy
typescript: 4.9.4
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
dev: true
/@commitlint/message/17.4.0:
resolution: {integrity: sha512-USGJDU9PPxcgQjKXCzvPUal65KAhxWq3hp+MrU1pNCN2itWM654CLIoY2LMIQ7rScTli9B5dTLH3vXhzbItmzA==}
engines: {node: '>=v14'}
dev: true
/@commitlint/parse/17.4.0:
resolution: {integrity: sha512-x8opKc5p+Hgs+CrMbq3VAnW2L2foPAX6arW8u9c8nTzksldGgFsENT+XVyPmpSMLlVBswZ1tndcz1xyKiY9TJA==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/types': 17.4.0
conventional-changelog-angular: 5.0.13
conventional-commits-parser: 3.2.4
dev: true
/@commitlint/read/17.4.0:
resolution: {integrity: sha512-pGDeZpbkyvhxK8ZoCDUacPPRpauKPWF3n2XpDBEnuGreqUF2clq2PVJpwMMaNN5cHW8iFKCbcoOjXhD01sln0A==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/top-level': 17.4.0
'@commitlint/types': 17.4.0
fs-extra: 11.1.0
git-raw-commits: 2.0.11
minimist: 1.2.6
dev: true
/@commitlint/resolve-extends/16.2.1: /@commitlint/resolve-extends/16.2.1:
resolution: {integrity: sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg==} resolution: {integrity: sha512-NbbCMPKTFf2J805kwfP9EO+vV+XvnaHRcBy6ud5dF35dxMsvdJqke54W3XazXF1ZAxC4a3LBy4i/GNVBAthsEg==}
engines: {node: '>=v12'} engines: {node: '>=v12'}
...@@ -81,6 +217,41 @@ packages: ...@@ -81,6 +217,41 @@ packages:
dev: true dev: true
optional: true optional: true
/@commitlint/resolve-extends/17.4.0:
resolution: {integrity: sha512-3JsmwkrCzoK8sO22AzLBvNEvC1Pmdn/65RKXzEtQMy6oYMl0Snrq97a5bQQEFETF0VsvbtUuKttLqqgn99OXRQ==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/config-validator': 17.4.0
'@commitlint/types': 17.4.0
import-fresh: 3.3.0
lodash.mergewith: 4.6.2
resolve-from: 5.0.0
resolve-global: 1.0.0
dev: true
/@commitlint/rules/17.4.0:
resolution: {integrity: sha512-lz3i1jet2NNjTWpAMwjjQjMZCPWBIHK1Kkja9o09UmUtMjRdALTb8uMLe8gCyeq3DiiZ5lLYOhbsoPK56xGQKA==}
engines: {node: '>=v14'}
dependencies:
'@commitlint/ensure': 17.4.0
'@commitlint/message': 17.4.0
'@commitlint/to-lines': 17.4.0
'@commitlint/types': 17.4.0
execa: 5.1.1
dev: true
/@commitlint/to-lines/17.4.0:
resolution: {integrity: sha512-LcIy/6ZZolsfwDUWfN1mJ+co09soSuNASfKEU5sCmgFCvX5iHwRYLiIuoqXzOVDYOy7E7IcHilr/KS0e5T+0Hg==}
engines: {node: '>=v14'}
dev: true
/@commitlint/top-level/17.4.0:
resolution: {integrity: sha512-/1loE/g+dTTQgHnjoCy0AexKAEFyHsR2zRB4NWrZ6lZSMIxAhBJnmCqwao7b4H8888PsfoTBCLBYIw8vGnej8g==}
engines: {node: '>=v14'}
dependencies:
find-up: 5.0.0
dev: true
/@commitlint/types/16.2.1: /@commitlint/types/16.2.1:
resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==} resolution: {integrity: sha512-7/z7pA7BM0i8XvMSBynO7xsB3mVQPUZbVn6zMIlp/a091XJ3qAXRXc+HwLYhiIdzzS5fuxxNIHZMGHVD4HJxdA==}
engines: {node: '>=v12'} engines: {node: '>=v12'}
...@@ -89,44 +260,56 @@ packages: ...@@ -89,44 +260,56 @@ packages:
dev: true dev: true
optional: true optional: true
/@cspotcode/source-map-consumer/0.8.0: /@commitlint/types/17.4.0:
resolution: {integrity: sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==} resolution: {integrity: sha512-2NjAnq5IcxY9kXtUeO2Ac0aPpvkuOmwbH/BxIm36XXK5LtWFObWJWjXOA+kcaABMrthjWu6la+FUpyYFMHRvbA==}
engines: {node: '>= 12'} engines: {node: '>=v14'}
dependencies:
chalk: 4.1.2
dev: true dev: true
optional: true
/@cspotcode/source-map-support/0.7.0: /@cspotcode/source-map-support/0.8.1:
resolution: {integrity: sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
'@cspotcode/source-map-consumer': 0.8.0 '@jridgewell/trace-mapping': 0.3.9
dev: true dev: true
optional: true
/@hutson/parse-repository-url/3.0.2: /@hutson/parse-repository-url/3.0.2:
resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
dev: true dev: true
/@jridgewell/resolve-uri/3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
dev: true
/@jridgewell/sourcemap-codec/1.4.14:
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
dev: true
/@jridgewell/trace-mapping/0.3.9:
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@tsconfig/node10/1.0.8: /@tsconfig/node10/1.0.8:
resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
dev: true dev: true
optional: true
/@tsconfig/node12/1.0.9: /@tsconfig/node12/1.0.9:
resolution: {integrity: sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==} resolution: {integrity: sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==}
dev: true dev: true
optional: true
/@tsconfig/node14/1.0.1: /@tsconfig/node14/1.0.1:
resolution: {integrity: sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==} resolution: {integrity: sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==}
dev: true dev: true
optional: true
/@tsconfig/node16/1.0.2: /@tsconfig/node16/1.0.2:
resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==}
dev: true dev: true
optional: true
/@types/minimist/1.2.2: /@types/minimist/1.2.2:
resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==}
...@@ -135,7 +318,6 @@ packages: ...@@ -135,7 +318,6 @@ packages:
/@types/node/17.0.29: /@types/node/17.0.29:
resolution: {integrity: sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==} resolution: {integrity: sha512-tx5jMmMFwx7wBwq/V7OohKDVb/JwJU5qCVkeLMh1//xycAJ/ESuw9aJ9SEtlCZDYi2pBfe4JkisSoAtbOsBNAA==}
dev: true dev: true
optional: true
/@types/normalize-package-data/2.4.1: /@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
...@@ -158,14 +340,12 @@ packages: ...@@ -158,14 +340,12 @@ packages:
resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dev: true dev: true
optional: true
/acorn/8.7.1: /acorn/8.7.1:
resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
dev: true dev: true
optional: true
/add-stream/1.0.0: /add-stream/1.0.0:
resolution: {integrity: sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=} resolution: {integrity: sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=}
...@@ -181,11 +361,27 @@ packages: ...@@ -181,11 +361,27 @@ packages:
dev: true dev: true
optional: true optional: true
/ajv/8.12.0:
resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js: 4.4.1
dev: true
/ansi-escapes/3.2.0: /ansi-escapes/3.2.0:
resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/ansi-escapes/4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
dependencies:
type-fest: 0.21.3
dev: true
/ansi-regex/3.0.1: /ansi-regex/3.0.1:
resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
engines: {node: '>=4'} engines: {node: '>=4'}
...@@ -218,21 +414,41 @@ packages: ...@@ -218,21 +414,41 @@ packages:
/arg/4.1.3: /arg/4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true dev: true
optional: true
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/array-ify/1.0.0: /array-ify/1.0.0:
resolution: {integrity: sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=} resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
dev: true dev: true
/arrify/1.0.1: /arrify/1.0.1:
resolution: {integrity: sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=} resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/at-least-node/1.0.0:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
dev: true
/balanced-match/1.0.2: /balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true dev: true
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
/bl/4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.0
dev: true
/brace-expansion/1.1.11: /brace-expansion/1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies: dependencies:
...@@ -247,8 +463,15 @@ packages: ...@@ -247,8 +463,15 @@ packages:
fill-range: 7.0.1 fill-range: 7.0.1
dev: true dev: true
/cachedir/2.2.0: /buffer/5.7.1:
resolution: {integrity: sha512-VvxA0xhNqIIfg0V9AmJkDg91DaJwryutH5rVEZAhcNi4iJFj9f+QxmAjgK1LT9I8OgToX27fypX6/MeCXVbBjQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: true
/cachedir/2.3.0:
resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
...@@ -256,7 +479,6 @@ packages: ...@@ -256,7 +479,6 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
optional: true
/camelcase-keys/6.2.2: /camelcase-keys/6.2.2:
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
...@@ -288,23 +510,39 @@ packages: ...@@ -288,23 +510,39 @@ packages:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
dev: true dev: true
optional: true
/chardet/0.7.0: /chardet/0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true dev: true
/cli-cursor/2.1.0: /cli-cursor/2.1.0:
resolution: {integrity: sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=} resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
restore-cursor: 2.0.0 restore-cursor: 2.0.0
dev: true dev: true
/cli-cursor/3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
dependencies:
restore-cursor: 3.1.0
dev: true
/cli-spinners/2.7.0:
resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==}
engines: {node: '>=6'}
dev: true
/cli-width/2.2.1: /cli-width/2.2.1:
resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==}
dev: true dev: true
/cli-width/3.0.0:
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
engines: {node: '>= 10'}
dev: true
/cliui/7.0.4: /cliui/7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
dependencies: dependencies:
...@@ -313,6 +551,20 @@ packages: ...@@ -313,6 +551,20 @@ packages:
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
dev: true dev: true
/cliui/8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
dev: true
/clone/1.0.4:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
dev: true
/color-convert/1.9.3: /color-convert/1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies: dependencies:
...@@ -327,32 +579,32 @@ packages: ...@@ -327,32 +579,32 @@ packages:
dev: true dev: true
/color-name/1.1.3: /color-name/1.1.3:
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: true dev: true
/color-name/1.1.4: /color-name/1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true dev: true
/commitizen/4.2.4: /commitizen/4.2.6:
resolution: {integrity: sha512-LlZChbDzg3Ir3O2S7jSo/cgWp5/QwylQVr59K4xayVq8S4/RdKzSyJkghAiZZHfhh5t4pxunUoyeg0ml1q/7aw==} resolution: {integrity: sha512-RyTM+EiD9GO01DJUn9MRRAet3XUHGfoUZoksLfr+1ym1Xt2q5EYJs9Fg2BtKSb5Mo53i0BtMBmWMHQXVlZ/L9w==}
engines: {node: '>= 10'} engines: {node: '>= 12'}
hasBin: true hasBin: true
dependencies: dependencies:
cachedir: 2.2.0 cachedir: 2.3.0
cz-conventional-changelog: 3.2.0 cz-conventional-changelog: 3.3.0
dedent: 0.7.0 dedent: 0.7.0
detect-indent: 6.0.0 detect-indent: 6.1.0
find-node-modules: 2.1.3 find-node-modules: 2.1.3
find-root: 1.1.0 find-root: 1.1.0
fs-extra: 8.1.0 fs-extra: 9.1.0
glob: 7.1.4 glob: 7.2.3
inquirer: 6.5.2 inquirer: 8.2.4
is-utf8: 0.2.1 is-utf8: 0.2.1
lodash: 4.17.21 lodash: 4.17.21
minimist: 1.2.5 minimist: 1.2.6
strip-bom: 4.0.0 strip-bom: 4.0.0
strip-json-comments: 3.0.1 strip-json-comments: 3.1.1
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/core' - '@swc/core'
- '@swc/wasm' - '@swc/wasm'
...@@ -366,7 +618,7 @@ packages: ...@@ -366,7 +618,7 @@ packages:
dev: true dev: true
/concat-map/0.0.1: /concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
/conventional-changelog-angular/5.0.13: /conventional-changelog-angular/5.0.13:
...@@ -412,6 +664,15 @@ packages: ...@@ -412,6 +664,15 @@ packages:
q: 1.5.1 q: 1.5.1
dev: true dev: true
/conventional-changelog-conventionalcommits/5.0.0:
resolution: {integrity: sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==}
engines: {node: '>=10'}
dependencies:
compare-func: 2.0.0
lodash: 4.17.21
q: 1.5.1
dev: true
/conventional-changelog-core/4.2.4: /conventional-changelog-core/4.2.4:
resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -523,8 +784,8 @@ packages: ...@@ -523,8 +784,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
is-text-path: 1.0.1
JSONStream: 1.3.5 JSONStream: 1.3.5
is-text-path: 1.0.1
lodash: 4.17.21 lodash: 4.17.21
meow: 8.1.2 meow: 8.1.2
split2: 3.2.2 split2: 3.2.2
...@@ -535,7 +796,7 @@ packages: ...@@ -535,7 +796,7 @@ packages:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true dev: true
/cosmiconfig-typescript-loader/1.0.9_5281fe59fc32158e106b8b5e2bebb315: /cosmiconfig-typescript-loader/1.0.9_kka74wp4giky4edlrnpcx25tcu:
resolution: {integrity: sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==} resolution: {integrity: sha512-tRuMRhxN4m1Y8hP9SNYfz7jRwt8lZdWxdjg/ohg5esKmsndJIn4yT96oJVcf5x0eA11taXl+sIp+ielu529k6g==}
engines: {node: '>=12', npm: '>=6'} engines: {node: '>=12', npm: '>=6'}
peerDependencies: peerDependencies:
...@@ -544,7 +805,7 @@ packages: ...@@ -544,7 +805,7 @@ packages:
dependencies: dependencies:
'@types/node': 17.0.29 '@types/node': 17.0.29
cosmiconfig: 7.0.1 cosmiconfig: 7.0.1
ts-node: 10.7.0_5281fe59fc32158e106b8b5e2bebb315 ts-node: 10.9.1_kka74wp4giky4edlrnpcx25tcu
typescript: 4.6.3 typescript: 4.6.3
transitivePeerDependencies: transitivePeerDependencies:
- '@swc/core' - '@swc/core'
...@@ -552,6 +813,21 @@ packages: ...@@ -552,6 +813,21 @@ packages:
dev: true dev: true
optional: true optional: true
/cosmiconfig-typescript-loader/4.3.0_asoaktcvsaefgrgziobsocgewq:
resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==}
engines: {node: '>=12', npm: '>=6'}
peerDependencies:
'@types/node': '*'
cosmiconfig: '>=7'
ts-node: '>=10'
typescript: '>=3'
dependencies:
'@types/node': 17.0.29
cosmiconfig: 8.0.0
ts-node: 10.9.1_u3h3q7xqdg7nitfvwf64wuo7zy
typescript: 4.9.4
dev: true
/cosmiconfig/7.0.1: /cosmiconfig/7.0.1:
resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -564,26 +840,27 @@ packages: ...@@ -564,26 +840,27 @@ packages:
dev: true dev: true
optional: true optional: true
/cosmiconfig/8.0.0:
resolution: {integrity: sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==}
engines: {node: '>=14'}
dependencies:
import-fresh: 3.3.0
js-yaml: 4.1.0
parse-json: 5.2.0
path-type: 4.0.0
dev: true
/create-require/1.1.1: /create-require/1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true dev: true
optional: true
/cz-conventional-changelog/3.2.0: /cross-spawn/7.0.3:
resolution: {integrity: sha512-yAYxeGpVi27hqIilG1nh4A9Bnx4J3Ov+eXy4koL3drrR+IO9GaWPsKjik20ht608Asqi8TQPf0mczhEeyAtMzg==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 10'} engines: {node: '>= 8'}
dependencies: dependencies:
chalk: 2.4.2 path-key: 3.1.1
commitizen: 4.2.4 shebang-command: 2.0.0
conventional-commit-types: 3.0.0 which: 2.0.2
lodash.map: 4.6.0
longest: 2.0.1
word-wrap: 1.2.3
optionalDependencies:
'@commitlint/load': 16.2.3
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
dev: true dev: true
/cz-conventional-changelog/3.3.0: /cz-conventional-changelog/3.3.0:
...@@ -591,7 +868,7 @@ packages: ...@@ -591,7 +868,7 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
dependencies: dependencies:
chalk: 2.4.2 chalk: 2.4.2
commitizen: 4.2.4 commitizen: 4.2.6
conventional-commit-types: 3.0.0 conventional-commit-types: 3.0.0
lodash.map: 4.6.0 lodash.map: 4.6.0
longest: 2.0.1 longest: 2.0.1
...@@ -603,6 +880,22 @@ packages: ...@@ -603,6 +880,22 @@ packages:
- '@swc/wasm' - '@swc/wasm'
dev: true dev: true
/cz-customizable/7.0.0:
resolution: {integrity: sha512-pQKkGSm+8SY9VY/yeJqDOla1MjrGaG7WG4EYLLEV4VNctGO7WdzdGtWEr2ydKSkrpmTs7f8fmBksg/FaTrUAyw==}
hasBin: true
dependencies:
editor: 1.0.0
find-config: 1.0.0
inquirer: 6.5.2
lodash: 4.17.21
temp: 0.9.4
word-wrap: 1.2.3
dev: true
/cz-git/1.4.1:
resolution: {integrity: sha512-EOtuitcnfxde8t3NNTKh2YxEJhLXGiVlKSVaZipK3+DVo135rEUifAfqxkslM66Nf6ZO7a+3JR+XAOLhMXUAjQ==}
dev: true
/dargs/7.0.0: /dargs/7.0.0:
resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -613,7 +906,7 @@ packages: ...@@ -613,7 +906,7 @@ packages:
dev: true dev: true
/decamelize-keys/1.1.0: /decamelize-keys/1.1.0:
resolution: {integrity: sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=} resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
decamelize: 1.2.0 decamelize: 1.2.0
...@@ -621,21 +914,27 @@ packages: ...@@ -621,21 +914,27 @@ packages:
dev: true dev: true
/decamelize/1.2.0: /decamelize/1.2.0:
resolution: {integrity: sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=} resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/dedent/0.7.0: /dedent/0.7.0:
resolution: {integrity: sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=} resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
dev: true
/defaults/1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
dependencies:
clone: 1.0.4
dev: true dev: true
/detect-file/1.0.0: /detect-file/1.0.0:
resolution: {integrity: sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=} resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/detect-indent/6.0.0: /detect-indent/6.1.0:
resolution: {integrity: sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==} resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
...@@ -643,7 +942,6 @@ packages: ...@@ -643,7 +942,6 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
dev: true dev: true
optional: true
/dot-prop/5.3.0: /dot-prop/5.3.0:
resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
...@@ -652,6 +950,10 @@ packages: ...@@ -652,6 +950,10 @@ packages:
is-obj: 2.0.0 is-obj: 2.0.0
dev: true dev: true
/editor/1.0.0:
resolution: {integrity: sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw==}
dev: true
/emoji-regex/8.0.0: /emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: true dev: true
...@@ -668,12 +970,27 @@ packages: ...@@ -668,12 +970,27 @@ packages:
dev: true dev: true
/escape-string-regexp/1.0.5: /escape-string-regexp/1.0.5:
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
dev: true dev: true
/execa/5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
dependencies:
cross-spawn: 7.0.3
get-stream: 6.0.1
human-signals: 2.1.0
is-stream: 2.0.1
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
dev: true
/expand-tilde/2.0.2: /expand-tilde/2.0.2:
resolution: {integrity: sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=} resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
homedir-polyfill: 1.0.3 homedir-polyfill: 1.0.3
...@@ -691,7 +1008,6 @@ packages: ...@@ -691,7 +1008,6 @@ packages:
/fast-deep-equal/3.1.3: /fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true dev: true
optional: true
/fast-json-stable-stringify/2.1.0: /fast-json-stable-stringify/2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
...@@ -699,12 +1015,19 @@ packages: ...@@ -699,12 +1015,19 @@ packages:
optional: true optional: true
/figures/2.0.0: /figures/2.0.0:
resolution: {integrity: sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=} resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
escape-string-regexp: 1.0.5 escape-string-regexp: 1.0.5
dev: true dev: true
/figures/3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/fill-range/7.0.1: /fill-range/7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -712,6 +1035,13 @@ packages: ...@@ -712,6 +1035,13 @@ packages:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
dev: true dev: true
/find-config/1.0.0:
resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==}
engines: {node: '>= 0.12'}
dependencies:
user-home: 2.0.0
dev: true
/find-node-modules/2.1.3: /find-node-modules/2.1.3:
resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
dependencies: dependencies:
...@@ -724,7 +1054,7 @@ packages: ...@@ -724,7 +1054,7 @@ packages:
dev: true dev: true
/find-up/2.1.0: /find-up/2.1.0:
resolution: {integrity: sha1-RdG35QbHF93UgndaK3eSCjwMV6c=} resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
locate-path: 2.0.0 locate-path: 2.0.0
...@@ -738,6 +1068,14 @@ packages: ...@@ -738,6 +1068,14 @@ packages:
path-exists: 4.0.0 path-exists: 4.0.0
dev: true dev: true
/find-up/5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
dependencies:
locate-path: 6.0.0
path-exists: 4.0.0
dev: true
/findup-sync/4.0.0: /findup-sync/4.0.0:
resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -748,17 +1086,27 @@ packages: ...@@ -748,17 +1086,27 @@ packages:
resolve-dir: 1.0.1 resolve-dir: 1.0.1
dev: true dev: true
/fs-extra/8.1.0: /fs-extra/11.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==}
engines: {node: '>=6 <7 || >=8'} engines: {node: '>=14.14'}
dependencies: dependencies:
graceful-fs: 4.2.10 graceful-fs: 4.2.10
jsonfile: 4.0.0 jsonfile: 6.1.0
universalify: 0.1.2 universalify: 2.0.0
dev: true
/fs-extra/9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: true dev: true
/fs.realpath/1.0.0: /fs.realpath/1.0.0:
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true dev: true
/function-bind/1.1.1: /function-bind/1.1.1:
...@@ -781,6 +1129,11 @@ packages: ...@@ -781,6 +1129,11 @@ packages:
yargs: 16.2.0 yargs: 16.2.0
dev: true dev: true
/get-stream/6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
/git-raw-commits/2.0.11: /git-raw-commits/2.0.11:
resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -816,8 +1169,8 @@ packages: ...@@ -816,8 +1169,8 @@ packages:
ini: 1.3.8 ini: 1.3.8
dev: true dev: true
/glob/7.1.4: /glob/7.2.3:
resolution: {integrity: sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
dependencies: dependencies:
fs.realpath: 1.0.0 fs.realpath: 1.0.0
inflight: 1.0.6 inflight: 1.0.6
...@@ -828,12 +1181,11 @@ packages: ...@@ -828,12 +1181,11 @@ packages:
dev: true dev: true
/global-dirs/0.1.1: /global-dirs/0.1.1:
resolution: {integrity: sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=} resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
ini: 1.3.8 ini: 1.3.8
dev: true dev: true
optional: true
/global-modules/1.0.0: /global-modules/1.0.0:
resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
...@@ -845,7 +1197,7 @@ packages: ...@@ -845,7 +1197,7 @@ packages:
dev: true dev: true
/global-prefix/1.0.2: /global-prefix/1.0.2:
resolution: {integrity: sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=} resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
expand-tilde: 2.0.2 expand-tilde: 2.0.2
...@@ -878,7 +1230,7 @@ packages: ...@@ -878,7 +1230,7 @@ packages:
dev: true dev: true
/has-flag/3.0.0: /has-flag/3.0.0:
resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
...@@ -886,7 +1238,6 @@ packages: ...@@ -886,7 +1238,6 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
optional: true
/has/1.0.3: /has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
...@@ -913,6 +1264,11 @@ packages: ...@@ -913,6 +1264,11 @@ packages:
lru-cache: 6.0.0 lru-cache: 6.0.0
dev: true dev: true
/human-signals/2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
/iconv-lite/0.4.24: /iconv-lite/0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -920,6 +1276,10 @@ packages: ...@@ -920,6 +1276,10 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: true dev: true
/ieee754/1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
/import-fresh/3.3.0: /import-fresh/3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -927,7 +1287,6 @@ packages: ...@@ -927,7 +1287,6 @@ packages:
parent-module: 1.0.1 parent-module: 1.0.1
resolve-from: 4.0.0 resolve-from: 4.0.0
dev: true dev: true
optional: true
/indent-string/4.0.0: /indent-string/4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
...@@ -935,7 +1294,7 @@ packages: ...@@ -935,7 +1294,7 @@ packages:
dev: true dev: true
/inflight/1.0.6: /inflight/1.0.6:
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
wrappy: 1.0.2 wrappy: 1.0.2
...@@ -968,8 +1327,29 @@ packages: ...@@ -968,8 +1327,29 @@ packages:
through: 2.3.8 through: 2.3.8
dev: true dev: true
/inquirer/8.2.4:
resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==}
engines: {node: '>=12.0.0'}
dependencies:
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-cursor: 3.1.0
cli-width: 3.0.0
external-editor: 3.1.0
figures: 3.2.0
lodash: 4.17.21
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.8.0
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
wrap-ansi: 7.0.0
dev: true
/is-arrayish/0.2.1: /is-arrayish/0.2.1:
resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true dev: true
/is-core-module/2.9.0: /is-core-module/2.9.0:
...@@ -979,12 +1359,12 @@ packages: ...@@ -979,12 +1359,12 @@ packages:
dev: true dev: true
/is-extglob/2.1.1: /is-extglob/2.1.1:
resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/is-fullwidth-code-point/2.0.0: /is-fullwidth-code-point/2.0.0:
resolution: {integrity: sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=} resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
...@@ -1000,6 +1380,11 @@ packages: ...@@ -1000,6 +1380,11 @@ packages:
is-extglob: 2.1.1 is-extglob: 2.1.1
dev: true dev: true
/is-interactive/1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
dev: true
/is-number/7.0.0: /is-number/7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
...@@ -1011,19 +1396,29 @@ packages: ...@@ -1011,19 +1396,29 @@ packages:
dev: true dev: true
/is-plain-obj/1.1.0: /is-plain-obj/1.1.0:
resolution: {integrity: sha1-caUMhCnfync8kqOQpKA7OfzVHT4=} resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/is-stream/2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: true
/is-text-path/1.0.1: /is-text-path/1.0.1:
resolution: {integrity: sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=} resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
text-extensions: 1.9.0 text-extensions: 1.9.0
dev: true dev: true
/is-unicode-supported/0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
dev: true
/is-utf8/0.2.1: /is-utf8/0.2.1:
resolution: {integrity: sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=} resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
dev: true dev: true
/is-windows/1.0.2: /is-windows/1.0.2:
...@@ -1036,13 +1431,20 @@ packages: ...@@ -1036,13 +1431,20 @@ packages:
dev: true dev: true
/isexe/2.0.0: /isexe/2.0.0:
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true dev: true
/js-tokens/4.0.0: /js-tokens/4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true dev: true
/js-yaml/4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
/json-parse-better-errors/1.0.2: /json-parse-better-errors/1.0.2:
resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==}
dev: true dev: true
...@@ -1056,18 +1458,24 @@ packages: ...@@ -1056,18 +1458,24 @@ packages:
dev: true dev: true
optional: true optional: true
/json-schema-traverse/1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
dev: true
/json-stringify-safe/5.0.1: /json-stringify-safe/5.0.1:
resolution: {integrity: sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=} resolution: {integrity: sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=}
dev: true dev: true
/jsonfile/4.0.0: /jsonfile/6.1.0:
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=} resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
universalify: 2.0.0
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.10 graceful-fs: 4.2.10
dev: true dev: true
/jsonparse/1.3.1: /jsonparse/1.3.1:
resolution: {integrity: sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=} resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0} engines: {'0': node >= 0.2.0}
dev: true dev: true
...@@ -1091,7 +1499,7 @@ packages: ...@@ -1091,7 +1499,7 @@ packages:
dev: true dev: true
/locate-path/2.0.0: /locate-path/2.0.0:
resolution: {integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=} resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
p-locate: 2.0.0 p-locate: 2.0.0
...@@ -1105,18 +1513,73 @@ packages: ...@@ -1105,18 +1513,73 @@ packages:
p-locate: 4.1.0 p-locate: 4.1.0
dev: true dev: true
/locate-path/6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
dependencies:
p-locate: 5.0.0
dev: true
/lodash.camelcase/4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
dev: true
/lodash.isfunction/3.0.9:
resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==}
dev: true
/lodash.ismatch/4.4.0: /lodash.ismatch/4.4.0:
resolution: {integrity: sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=} resolution: {integrity: sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=}
dev: true dev: true
/lodash.isplainobject/4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
/lodash.kebabcase/4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
dev: true
/lodash.map/4.6.0: /lodash.map/4.6.0:
resolution: {integrity: sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=} resolution: {integrity: sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=}
dev: true dev: true
/lodash.merge/4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash.mergewith/4.6.2:
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
dev: true
/lodash.snakecase/4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
dev: true
/lodash.startcase/4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
dev: true
/lodash.uniq/4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
dev: true
/lodash.upperfirst/4.3.1:
resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==}
dev: true
/lodash/4.17.21: /lodash/4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true dev: true
/log-symbols/4.1.0:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
dependencies:
chalk: 4.1.2
is-unicode-supported: 0.1.0
dev: true
/longest/2.0.1: /longest/2.0.1:
resolution: {integrity: sha1-eB4YMpaqlPbU2RbcM10NF676I/g=} resolution: {integrity: sha1-eB4YMpaqlPbU2RbcM10NF676I/g=}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -1132,10 +1595,9 @@ packages: ...@@ -1132,10 +1595,9 @@ packages:
/make-error/1.3.6: /make-error/1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: true dev: true
optional: true
/map-obj/1.0.1: /map-obj/1.0.1:
resolution: {integrity: sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=} resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
...@@ -1161,6 +1623,10 @@ packages: ...@@ -1161,6 +1623,10 @@ packages:
yargs-parser: 20.2.9 yargs-parser: 20.2.9
dev: true dev: true
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/merge/2.1.1: /merge/2.1.1:
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
dev: true dev: true
...@@ -1178,6 +1644,11 @@ packages: ...@@ -1178,6 +1644,11 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
/min-indent/1.0.1: /min-indent/1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'} engines: {node: '>=4'}
...@@ -1198,21 +1669,28 @@ packages: ...@@ -1198,21 +1669,28 @@ packages:
kind-of: 6.0.3 kind-of: 6.0.3
dev: true dev: true
/minimist/1.2.5:
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
dev: true
/minimist/1.2.6: /minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true dev: true
/mkdirp/0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
minimist: 1.2.6
dev: true
/modify-values/1.0.1: /modify-values/1.0.1:
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/mute-stream/0.0.7: /mute-stream/0.0.7:
resolution: {integrity: sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=} resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==}
dev: true
/mute-stream/0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
dev: true dev: true
/neo-async/2.6.2: /neo-async/2.6.2:
...@@ -1234,25 +1712,59 @@ packages: ...@@ -1234,25 +1712,59 @@ packages:
dependencies: dependencies:
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
is-core-module: 2.9.0 is-core-module: 2.9.0
semver: 7.3.7 semver: 7.3.8
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
dev: true dev: true
/npm-run-path/4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
dev: true dev: true
/onetime/2.0.1: /onetime/2.0.1:
resolution: {integrity: sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=} resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
mimic-fn: 1.2.0 mimic-fn: 1.2.0
dev: true dev: true
/onetime/5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
/ora/5.4.1:
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
engines: {node: '>=10'}
dependencies:
bl: 4.1.0
chalk: 4.1.2
cli-cursor: 3.1.0
cli-spinners: 2.7.0
is-interactive: 1.0.0
is-unicode-supported: 0.1.0
log-symbols: 4.1.0
strip-ansi: 6.0.1
wcwidth: 1.0.1
dev: true
/os-homedir/1.0.2:
resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==}
engines: {node: '>=0.10.0'}
dev: true
/os-tmpdir/1.0.2: /os-tmpdir/1.0.2:
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
...@@ -1270,8 +1782,15 @@ packages: ...@@ -1270,8 +1782,15 @@ packages:
p-try: 2.2.0 p-try: 2.2.0
dev: true dev: true
/p-limit/3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
dependencies:
yocto-queue: 0.1.0
dev: true
/p-locate/2.0.0: /p-locate/2.0.0:
resolution: {integrity: sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=} resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
p-limit: 1.3.0 p-limit: 1.3.0
...@@ -1284,8 +1803,15 @@ packages: ...@@ -1284,8 +1803,15 @@ packages:
p-limit: 2.3.0 p-limit: 2.3.0
dev: true dev: true
/p-locate/5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
dependencies:
p-limit: 3.1.0
dev: true
/p-try/1.0.0: /p-try/1.0.0:
resolution: {integrity: sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=} resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
...@@ -1300,10 +1826,9 @@ packages: ...@@ -1300,10 +1826,9 @@ packages:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
dev: true dev: true
optional: true
/parse-json/4.0.0: /parse-json/4.0.0:
resolution: {integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=} resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
error-ex: 1.3.2 error-ex: 1.3.2
...@@ -1321,12 +1846,12 @@ packages: ...@@ -1321,12 +1846,12 @@ packages:
dev: true dev: true
/parse-passwd/1.0.0: /parse-passwd/1.0.0:
resolution: {integrity: sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=} resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/path-exists/3.0.0: /path-exists/3.0.0:
resolution: {integrity: sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=} resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
...@@ -1336,10 +1861,15 @@ packages: ...@@ -1336,10 +1861,15 @@ packages:
dev: true dev: true
/path-is-absolute/1.0.1: /path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/path-key/3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
dev: true
/path-parse/1.0.7: /path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true dev: true
...@@ -1355,7 +1885,6 @@ packages: ...@@ -1355,7 +1885,6 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
optional: true
/picomatch/2.3.1: /picomatch/2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
...@@ -1380,10 +1909,9 @@ packages: ...@@ -1380,10 +1909,9 @@ packages:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
optional: true
/q/1.5.1: /q/1.5.1:
resolution: {integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=} resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==}
engines: {node: '>=0.6.0', teleport: '>=0.2.0'} engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
dev: true dev: true
...@@ -1393,7 +1921,7 @@ packages: ...@@ -1393,7 +1921,7 @@ packages:
dev: true dev: true
/read-pkg-up/3.0.0: /read-pkg-up/3.0.0:
resolution: {integrity: sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=} resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
find-up: 2.1.0 find-up: 2.1.0
...@@ -1410,7 +1938,7 @@ packages: ...@@ -1410,7 +1938,7 @@ packages:
dev: true dev: true
/read-pkg/3.0.0: /read-pkg/3.0.0:
resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=} resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
load-json-file: 4.0.0 load-json-file: 4.0.0
...@@ -1458,12 +1986,17 @@ packages: ...@@ -1458,12 +1986,17 @@ packages:
dev: true dev: true
/require-directory/2.1.1: /require-directory/2.1.1:
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
dev: true
/require-from-string/2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/resolve-dir/1.0.1: /resolve-dir/1.0.1:
resolution: {integrity: sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=} resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dependencies: dependencies:
expand-tilde: 2.0.2 expand-tilde: 2.0.2
...@@ -1474,13 +2007,11 @@ packages: ...@@ -1474,13 +2007,11 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
optional: true
/resolve-from/5.0.0: /resolve-from/5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
optional: true
/resolve-global/1.0.0: /resolve-global/1.0.0:
resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==}
...@@ -1488,7 +2019,6 @@ packages: ...@@ -1488,7 +2019,6 @@ packages:
dependencies: dependencies:
global-dirs: 0.1.1 global-dirs: 0.1.1
dev: true dev: true
optional: true
/resolve/1.22.0: /resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
...@@ -1500,13 +2030,28 @@ packages: ...@@ -1500,13 +2030,28 @@ packages:
dev: true dev: true
/restore-cursor/2.0.0: /restore-cursor/2.0.0:
resolution: {integrity: sha1-n37ih/gv0ybU/RYpI9YhKe7g368=} resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
onetime: 2.0.1 onetime: 2.0.1
signal-exit: 3.0.7 signal-exit: 3.0.7
dev: true dev: true
/restore-cursor/3.1.0:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
dev: true
/rimraf/2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
hasBin: true
dependencies:
glob: 7.2.3
dev: true
/run-async/2.4.1: /run-async/2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
...@@ -1519,6 +2064,12 @@ packages: ...@@ -1519,6 +2064,12 @@ packages:
tslib: 1.14.1 tslib: 1.14.1
dev: true dev: true
/rxjs/7.8.0:
resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==}
dependencies:
tslib: 2.4.1
dev: true
/safe-buffer/5.1.2: /safe-buffer/5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: true dev: true
...@@ -1541,14 +2092,26 @@ packages: ...@@ -1541,14 +2092,26 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/semver/7.3.7: /semver/7.3.8:
resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
dev: true dev: true
/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
dev: true
/shebang-regex/3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
dev: true
/signal-exit/3.0.7: /signal-exit/3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
dev: true dev: true
...@@ -1622,7 +2185,7 @@ packages: ...@@ -1622,7 +2185,7 @@ packages:
dev: true dev: true
/strip-ansi/4.0.0: /strip-ansi/4.0.0:
resolution: {integrity: sha1-qEeQIusaw2iocTibY1JixQXuNo8=} resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
ansi-regex: 3.0.1 ansi-regex: 3.0.1
...@@ -1643,7 +2206,7 @@ packages: ...@@ -1643,7 +2206,7 @@ packages:
dev: true dev: true
/strip-bom/3.0.0: /strip-bom/3.0.0:
resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=} resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
...@@ -1652,6 +2215,11 @@ packages: ...@@ -1652,6 +2215,11 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
/strip-indent/3.0.0: /strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -1659,8 +2227,8 @@ packages: ...@@ -1659,8 +2227,8 @@ packages:
min-indent: 1.0.1 min-indent: 1.0.1
dev: true dev: true
/strip-json-comments/3.0.1: /strip-json-comments/3.1.1:
resolution: {integrity: sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
...@@ -1677,7 +2245,6 @@ packages: ...@@ -1677,7 +2245,6 @@ packages:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
dev: true dev: true
optional: true
/supports-preserve-symlinks-flag/1.0.0: /supports-preserve-symlinks-flag/1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
...@@ -1689,6 +2256,14 @@ packages: ...@@ -1689,6 +2256,14 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/temp/0.9.4:
resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==}
engines: {node: '>=6.0.0'}
dependencies:
mkdirp: 0.5.6
rimraf: 2.6.3
dev: true
/tempfile/3.0.0: /tempfile/3.0.0:
resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -1703,7 +2278,7 @@ packages: ...@@ -1703,7 +2278,7 @@ packages:
dev: true dev: true
/through/2.3.8: /through/2.3.8:
resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true dev: true
/through2/2.0.5: /through2/2.0.5:
...@@ -1738,8 +2313,8 @@ packages: ...@@ -1738,8 +2313,8 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/ts-node/10.7.0_5281fe59fc32158e106b8b5e2bebb315: /ts-node/10.9.1_kka74wp4giky4edlrnpcx25tcu:
resolution: {integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==} resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@swc/core': '>=1.2.50' '@swc/core': '>=1.2.50'
...@@ -1752,7 +2327,7 @@ packages: ...@@ -1752,7 +2327,7 @@ packages:
'@swc/wasm': '@swc/wasm':
optional: true optional: true
dependencies: dependencies:
'@cspotcode/source-map-support': 0.7.0 '@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.8 '@tsconfig/node10': 1.0.8
'@tsconfig/node12': 1.0.9 '@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1 '@tsconfig/node14': 1.0.1
...@@ -1770,15 +2345,55 @@ packages: ...@@ -1770,15 +2345,55 @@ packages:
dev: true dev: true
optional: true optional: true
/ts-node/10.9.1_u3h3q7xqdg7nitfvwf64wuo7zy:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.8
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 17.0.29
acorn: 8.7.1
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.9.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/tslib/1.14.1: /tslib/1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true dev: true
/tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: true
/type-fest/0.18.1: /type-fest/0.18.1:
resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==}
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/type-fest/0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
dev: true
/type-fest/0.6.0: /type-fest/0.6.0:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -1796,6 +2411,12 @@ packages: ...@@ -1796,6 +2411,12 @@ packages:
dev: true dev: true
optional: true optional: true
/typescript/4.9.4:
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
/uglify-js/3.15.4: /uglify-js/3.15.4:
resolution: {integrity: sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==} resolution: {integrity: sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
...@@ -1804,9 +2425,9 @@ packages: ...@@ -1804,9 +2425,9 @@ packages:
dev: true dev: true
optional: true optional: true
/universalify/0.1.2: /universalify/2.0.0:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 10.0.0'}
dev: true dev: true
/uri-js/4.4.1: /uri-js/4.4.1:
...@@ -1814,10 +2435,16 @@ packages: ...@@ -1814,10 +2435,16 @@ packages:
dependencies: dependencies:
punycode: 2.1.1 punycode: 2.1.1
dev: true dev: true
optional: true
/user-home/2.0.0:
resolution: {integrity: sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ==}
engines: {node: '>=0.10.0'}
dependencies:
os-homedir: 1.0.2
dev: true
/util-deprecate/1.0.2: /util-deprecate/1.0.2:
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true dev: true
/uuid/3.4.0: /uuid/3.4.0:
...@@ -1829,7 +2456,6 @@ packages: ...@@ -1829,7 +2456,6 @@ packages:
/v8-compile-cache-lib/3.0.1: /v8-compile-cache-lib/3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
dev: true dev: true
optional: true
/validate-npm-package-license/3.0.4: /validate-npm-package-license/3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
...@@ -1838,6 +2464,12 @@ packages: ...@@ -1838,6 +2464,12 @@ packages:
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
dev: true dev: true
/wcwidth/1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
dependencies:
defaults: 1.0.4
dev: true
/which/1.3.1: /which/1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true hasBin: true
...@@ -1845,6 +2477,14 @@ packages: ...@@ -1845,6 +2477,14 @@ packages:
isexe: 2.0.0 isexe: 2.0.0
dev: true dev: true
/which/2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
dependencies:
isexe: 2.0.0
dev: true
/word-wrap/1.2.3: /word-wrap/1.2.3:
resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -1864,7 +2504,7 @@ packages: ...@@ -1864,7 +2504,7 @@ packages:
dev: true dev: true
/wrappy/1.0.2: /wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true dev: true
/xtend/4.0.2: /xtend/4.0.2:
...@@ -1892,6 +2532,11 @@ packages: ...@@ -1892,6 +2532,11 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/yargs-parser/21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
dev: true
/yargs/16.2.0: /yargs/16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -1905,8 +2550,25 @@ packages: ...@@ -1905,8 +2550,25 @@ packages:
yargs-parser: 20.2.9 yargs-parser: 20.2.9
dev: true dev: true
/yargs/17.6.2:
resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
engines: {node: '>=12'}
dependencies:
cliui: 8.0.1
escalade: 3.1.1
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
dev: true
/yn/3.1.1: /yn/3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
optional: true
/yocto-queue/0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
...@@ -4,12 +4,16 @@ pluginManagement { ...@@ -4,12 +4,16 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
rootProject.name = 'basic-api'
rootProject.name = 'basic-api-boot'
include 'app' include 'app'
include 'basic-auth' include 'basic-auth'
include 'basic-dict'
include 'basic-websocket'
include 'basic-common:core' include 'basic-common:core'
include 'basic-common:util' include 'basic-common:util'
include 'basic-common:doc' include 'basic-common:doc'
include 'basic-common:minio' include 'basic-common:minio'
include 'basic-common:redis' include 'basic-common:redis'
include 'basic-common:i18n'
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论