提交 9c6d7531 作者: 方治民

feat: springboot v3 + openapi v3 等相关适配完成

上级 ccb648b8
......@@ -30,11 +30,11 @@ dependencies {
// Optional: Doc
implementation project(":basic-common:doc")
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// Optional: Auth
implementation project(":basic-auth")
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}"
implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
// Optional: WebSocket && STOMP 依赖 Auth + Redis 模块
implementation project(":basic-websocket")
......@@ -59,6 +59,6 @@ dependencies {
implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.app.config;
import cn.dev33.satoken.exception.*;
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 javax.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.HttpStatus;
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.ControllerAdvice;
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;
/**
* 全局错误处理
......@@ -33,130 +19,14 @@ import org.springframework.web.bind.annotation.ResponseStatus;
* 2017年11月30日 上午11:36:31
*/
@Slf4j
@ControllerAdvice
@ResponseBody
@Order
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
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) {
// TODO: 可以优化成提取 @ApiModelProperty value 中文描述
// 构建明确的字段错误提示, 例如: 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);
}
/**
* 不支持的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());
}
/**
* 未登录异常(鉴权失败)
*
* @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() {
return Result.no(Status.FORBIDDEN);
}
/**
* 自定义业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<String> businessExceptionHandler(BusinessException e) {
return Result.no(Status.BAD_REQUEST, e.getCode(), e.getMessage(), null);
}
/**
* 失败状态异常
*/
@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() {}
/**
* 其他异常
*
* @param e 异常信息
......
......@@ -3,12 +3,12 @@ package com.yiring.app.domain.user;
import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.io.Serial;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
......
......@@ -2,11 +2,13 @@
package com.yiring.app.vo.user;
import com.yiring.common.jackson.MappingSerialize;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
/**
......@@ -15,9 +17,8 @@ import lombok.experimental.FieldDefaults;
* 2022/7/13 11:36
*/
@ApiModel("UserExtensionVo")
@Schema(name = "UserExtensionVo", description = "用户扩展信息")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
......@@ -26,10 +27,13 @@ public class UserExtensionVo implements Serializable {
@Serial
private static final long serialVersionUID = -2251567849918281906L;
@ApiModelProperty(value = "性别", example = "男")
@Schema(description = "性别", example = "男", allowableValues = { "男", "女" }, type = "string")
@MappingSerialize(mapping = "0:女,1:男")
Integer gender;
@ApiModelProperty(value = "年龄", example = "18")
@Schema(description = "年龄", example = "18")
Integer age;
@Schema(description = "简介", example = "Hi")
String introduction;
}
......@@ -10,6 +10,7 @@ import com.yiring.app.domain.user.UserExtensionRepository;
import com.yiring.app.vo.user.UserExtensionVo;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
......@@ -19,16 +20,17 @@ import com.yiring.common.util.Commons;
import com.yiring.common.util.FileUtils;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.domain.Example;
import org.springframework.http.MediaType;
......@@ -43,9 +45,8 @@ import org.springframework.web.bind.annotation.*;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = 0)
@Api(tags = "示例", description = "Example")
@Tag(name = "示例", description = "Example")
@RequestMapping("/example/")
@RestController
@RequiredArgsConstructor
......@@ -55,7 +56,7 @@ public class ExampleController {
final Auths auths;
final UserExtensionRepository userExtensionRepository;
@ApiOperation("Hello World")
@Operation(summary = "Hello World")
@GetMapping
public Result<String> hello() {
return Result.ok("example.hello");
......@@ -64,16 +65,16 @@ public class ExampleController {
/**
* 测试失败自定义状态信息输出
*/
@ApiOperation("测试失败")
@Operation(summary = "测试失败")
@GetMapping("fail")
public Result<String> fail() {
throw BusinessException.i18n("Code.1");
}
@SaCheckLogin
@ApiOperation("分页条件查询")
@Operation(summary = "分页条件查询")
@GetMapping("page")
public Result<PageVo<String>> page(@Validated PageParam param) {
public Result<PageVo<String>> page(@ParameterObject @Validated PageParam param) {
log.debug("PageParam: {}", param);
String text = i18n.get("example.hello");
......@@ -82,7 +83,7 @@ public class ExampleController {
return Result.ok(vo);
}
@ApiOperation(value = "JSON 传参")
@Operation(summary = "JSON 传参")
@PostMapping("json")
public Result<PageVo<String>> json(
@RequestBody(required = false) @Validated(Group.Optional.class) PageParam param
......@@ -91,9 +92,10 @@ public class ExampleController {
}
@SaIgnore
@DownloadResponse
@SneakyThrows(IOException.class)
@ApiOperation(value = "文件下载", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@GetMapping("download")
@Operation(summary = "文件下载")
@GetMapping(value = "download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void download(HttpServletResponse response) {
ClassPathResource resource = new ClassPathResource("static/cat.jpg");
FileUtils.download(response, resource.getFile());
......@@ -101,7 +103,7 @@ public class ExampleController {
@SaCheckSafe
@SaCheckLogin
@ApiOperation("查询用户属性")
@Operation(summary = "查询用户属性")
@GetMapping("findUserExtensionInfo")
public Result<UserExtensionVo> findUserExtensionInfo() {
User user = auths.getLoginUser();
......
......@@ -19,3 +19,5 @@ logging:
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
......@@ -12,19 +12,34 @@ spring:
username: ${env.extra.username}
password: ${env.extra.password}
jpa:
database-platform: org.hibernate.dialect.PostgreSQL10Dialect
database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: true
hibernate:
ddl-auto: update
show-sql: true
show-sql: false
properties:
hibernate:
format_sql: true
redis:
database: 5
host: ${env.host}
port: 6379
password: ${env.extra.password}
# https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827
jakarta:
persistence:
sharedCache:
mode: UNSPECIFIED
data:
redis:
database: 5
port: 6379
host: ${env.host}
password: ${env.extra.password}
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: true
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
# knife4j
knife4j:
......
......@@ -3,3 +3,16 @@
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
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
......@@ -3,3 +3,16 @@
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.1=\u5931\u8D25
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
javax.validation.constraints.AssertFalse.message=\u53EA\u80FD\u4E3A false
javax.validation.constraints.AssertTrue.message=\u53EA\u80FD\u4E3A true
javax.validation.constraints.DecimalMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
javax.validation.constraints.DecimalMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}{value}
javax.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)
javax.validation.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
javax.validation.constraints.Future.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u7684\u65F6\u95F4
javax.validation.constraints.FutureOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u5C06\u6765\u6216\u73B0\u5728\u7684\u65F6\u95F4
javax.validation.constraints.Max.message=\u6700\u5927\u4E0D\u80FD\u8D85\u8FC7{value}
javax.validation.constraints.Min.message=\u6700\u5C0F\u4E0D\u80FD\u5C0F\u4E8E{value}
javax.validation.constraints.Negative.message=\u5FC5\u987B\u662F\u8D1F\u6570
javax.validation.constraints.NegativeOrZero.message=\u5FC5\u987B\u662F\u8D1F\u6570\u6216\u96F6
javax.validation.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
javax.validation.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
javax.validation.constraints.NotNull.message=\u4E0D\u80FD\u4E3A null
javax.validation.constraints.Null.message=\u5FC5\u987B\u4E3A null
javax.validation.constraints.Past.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65F6\u95F4
javax.validation.constraints.PastOrPresent.message=\u9700\u8981\u662F\u4E00\u4E2A\u8FC7\u53BB\u6216\u73B0\u5728\u7684\u65F6\u95F4
javax.validation.constraints.Pattern.message=\u9700\u8981\u5339\u914D\u6B63\u5219\u8868\u8FBE\u5F0F"{regexp}"
javax.validation.constraints.Positive.message=\u5FC5\u987B\u662F\u6B63\u6570
javax.validation.constraints.PositiveOrZero.message=\u5FC5\u987B\u662F\u6B63\u6570\u6216\u96F6
javax.validation.constraints.Size.message=\u4E2A\u6570\u5FC5\u987B\u5728{min}\u548C{max}\u4E4B\u95F4
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
......
......@@ -9,11 +9,13 @@ dependencies {
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// sa-token
implementation "cn.dev33:sa-token-spring-boot-starter:${saTokenVersion}"
implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
implementation "cn.dev33:sa-token-dao-redis-jackson:${saTokenVersion}"
// Sa-Token 整合 Redis (使用 jackson 序列化方式)
implementation 'org.apache.commons:commons-pool2'
// fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
......@@ -21,8 +23,11 @@ dependencies {
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
// https://github.com/vladmihalcea/hibernate-types
// hibernate-types-55
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
// hypersistence-utils-hibernate-60
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.annotation;
import cn.dev33.satoken.annotation.SaIgnore;
import java.lang.annotation.*;
/**
* 忽略登录校验
* 与 @SaCheckLogin 相对
*
* @author Jim
* @version 0.1
* 2022/4/7 15:21
* @deprecated 已过期,请使用 @SaIgnore
*/
@SuppressWarnings({ "unused" })
@Deprecated
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SaIgnore
public @interface AuthIgnore {
}
/* (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(0)
@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);
}
}
......@@ -12,7 +12,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
......@@ -24,10 +24,10 @@ import org.springframework.stereotype.Component;
*/
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
......@@ -50,6 +50,7 @@ public class StpInterfaceImpl implements StpInterface {
/**
* 根据 id 获取用户信息
*
* @param loginId 登录 ID
* @return 用户信息
*/
......@@ -57,7 +58,7 @@ public class StpInterfaceImpl implements StpInterface {
String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception("用户不存在");
throw Status.NOT_FOUND.exception("Code.1000");
}
return optional.get();
......
......@@ -6,9 +6,10 @@ import static com.yiring.auth.domain.permission.Permission.TABLE_NAME;
import com.alibaba.fastjson2.JSONObject;
import com.yiring.common.domain.BasicEntity;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
......@@ -99,7 +100,7 @@ public class Permission extends BasicEntity implements Serializable {
* 可用于扩展一些前端可能用到的路由参数
*/
@Comment("扩展元数据信息")
@org.hibernate.annotations.Type(type = "json")
@org.hibernate.annotations.Type(JsonType.class)
@Column(columnDefinition = "json")
JSONObject meta;
......@@ -123,6 +124,7 @@ public class Permission extends BasicEntity implements Serializable {
/**
* 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整
*
* @return JSON 格式 Meta 元数据
*/
public JSONObject getMetaJson() {
......
......@@ -8,11 +8,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
......
......@@ -4,12 +4,12 @@ package com.yiring.auth.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.role.Role;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("LoginParam")
@Schema(name = "LoginParam")
@Data
@Builder
@NoArgsConstructor
......@@ -27,11 +27,11 @@ public class LoginParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "账号(支持用户名/手机号/邮箱)", example = "admin", required = true)
@Parameter(description = "账号(支持用户名/手机号/邮箱)", example = "admin")
@NotEmpty(message = "账号不能为空")
String account;
@ApiModelProperty(value = "密码", example = "123456", required = true)
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -17,7 +17,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("RegisterParam")
@Schema(name = "RegisterParam")
@Data
@Builder
@NoArgsConstructor
......@@ -28,32 +28,32 @@ public class RegisterParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456895L;
@ApiModelProperty(value = "用户名", example = "admin", required = true)
@Parameter(description = "用户名", example = "admin")
@NotEmpty(message = "用户名不能为空")
String username;
@ApiModelProperty(value = "密码", example = "123456", required = true)
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
@ApiModelProperty(value = "真实姓名", example = "管理员", required = true)
@Parameter(description = "真实姓名", example = "管理员")
@NotEmpty(message = "真实姓名不能为空")
String realName;
@ApiModelProperty(value = "手机号", example = "13012345678", required = true)
@Parameter(description = "手机号", example = "13012345678")
@NotEmpty(message = "手机号不能为空")
@Pattern(regexp = "^1\\d{10}$", message = "手机号码格式不正确")
String mobile;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
@Parameter(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
@Parameter(description = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "简介", example = "平台管理员")
@Parameter(description = "简介", example = "平台管理员")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
@Parameter(description = "是否启用", example = "true")
Boolean enable;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.param.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("SafeParam")
@Schema(name = "SafeParam")
@Data
@Builder
@NoArgsConstructor
......@@ -27,7 +27,7 @@ public class SafeParam implements Serializable {
@Serial
private static final long serialVersionUID = 9106494470582579138L;
@ApiModelProperty(value = "密码", example = "123456", required = true)
@Parameter(description = "密码", example = "123456")
@NotEmpty(message = "密码不能为空")
String password;
}
......@@ -4,13 +4,13 @@ package com.yiring.auth.param.permission;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -23,7 +23,7 @@ import lombok.experimental.FieldDefaults;
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionParam")
@Schema(name = "PermissionParam")
@Data
@Builder
@NoArgsConstructor
......@@ -34,47 +34,47 @@ public class PermissionParam implements Serializable {
@Serial
private static final long serialVersionUID = -6781934969837655538L;
@ApiModelProperty(value = "id", example = "1")
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@ApiModelProperty(value = "权限类型", example = "MENU", required = true)
@Parameter(description = "权限类型", example = "MENU")
@NotNull(message = "权限类型不能为空")
Permission.Type type;
@ApiModelProperty(value = "序号", example = "1")
@Parameter(description = "序号", example = "1")
Integer serial;
@ApiModelProperty(value = "标识", example = "Dashboard", required = true)
@Parameter(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空")
String uid;
@ApiModelProperty(value = "名称", example = "Dashboard", required = true)
@Parameter(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空")
String name;
@ApiModelProperty(value = "路径", example = "/dashboard")
@Parameter(description = "路径", example = "/dashboard")
String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench")
@Parameter(description = "重定向", example = "/dashboard/workbench")
String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT")
@Parameter(description = "组件", example = "LAYOUT")
String component;
@ApiModelProperty(value = "图标", example = "ion:grid-outline")
@Parameter(description = "图标", example = "ion:grid-outline")
String icon;
@ApiModelProperty(value = "是否隐藏", example = "false")
@Parameter(description = "是否隐藏", example = "false")
Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true")
@Parameter(description = "是否启用", example = "true")
Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0")
@Parameter(description = "父级ID", example = "0")
@Builder.Default
String pid = "0";
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
@Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta;
}
......@@ -2,12 +2,12 @@
package com.yiring.auth.param.role;
import com.yiring.common.validation.group.Group;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -19,7 +19,7 @@ import lombok.experimental.FieldDefaults;
* 2022/3/25 17:09
*/
@ApiModel("RoleParam")
@Schema(name = "RoleParam")
@Data
@Builder
@NoArgsConstructor
......@@ -30,15 +30,15 @@ public class RoleParam implements Serializable {
@Serial
private static final long serialVersionUID = 6572751635422870217L;
@ApiModelProperty(value = "id", example = "1")
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@ApiModelProperty(value = "标识", example = "admin", required = true)
@Parameter(description = "标识", example = "admin")
@NotEmpty(message = "角色标识不能为空")
String uid;
@ApiModelProperty(value = "名称", example = "管理员", required = true)
@Parameter(description = "名称", example = "管理员")
@NotEmpty(message = "角色名称不能为空")
String name;
}
......@@ -9,8 +9,8 @@ import com.yiring.common.core.Status;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Resource;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
......@@ -23,10 +23,10 @@ import org.springframework.stereotype.Component;
@SuppressWarnings("unused")
@Component
@RequiredArgsConstructor
public class Auths {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
/**
* 管理员角色标识
......@@ -36,6 +36,7 @@ public class Auths {
/**
* 根据 Token 获取用户信息
* 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED}
*
* @param token token
* @return 用户信息
*/
......@@ -70,6 +71,7 @@ public class Auths {
/**
* 踢出这个用户 id 所有登录状态(可能有多人重复登录了一个账号的情况)
*
* @param userId 用户 id
*/
public void logoutAll(String userId) {
......@@ -81,6 +83,7 @@ public class Auths {
/**
* 判断用户是否为超级管理员
*
* @param userId 用户 ID
* @return 是否为管理员
*/
......@@ -91,6 +94,7 @@ public class Auths {
/**
* 检查用户是否为管理员(检查用户是否拥有包含 admin 字符的角色)
*
* @param user 用户
* @return 是否为管理员
*/
......@@ -104,6 +108,7 @@ public class Auths {
/**
* 检查当前登录用户是否为管理员
* {@link this.isAdmin}
*
* @return 是否为管理员
*/
public boolean checkLoginUserIsAdmin() {
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.util;
import com.sun.istack.Nullable;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.util.Commons;
import jakarta.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
......@@ -28,6 +28,7 @@ public class Permissions {
/**
* 将角色集合转换成 Vo 集合
*
* @param roles 角色集合
* @return vos
*/
......@@ -40,6 +41,7 @@ public class Permissions {
/**
* 将权限集合转换成菜单树
*
* @param permissions 权限集合
* @return 菜单树
*/
......@@ -75,6 +77,7 @@ public class Permissions {
/**
* 菜单树递归排序
*
* @param menus 菜单集合
* @return 排序后的菜单集合
*/
......@@ -88,7 +91,7 @@ public class Permissions {
)
)
.peek(item -> {
if (!Commons.isNullOrEmpty(item.getChildren())) {
if (Commons.notEmpty(item.getChildren())) {
item.setChildren(sortMenuTreeVo(item.getChildren()));
}
})
......@@ -97,6 +100,7 @@ public class Permissions {
/**
* 将权限集合转换成 Vo 集合
*
* @param permissions 权限集合
* @return vos
*/
......@@ -114,6 +118,7 @@ public class Permissions {
/**
* 提取角色集合含有的权限去重结果
*
* @param roles 角色集合
* @return 权限集合
*/
......@@ -130,8 +135,9 @@ public class Permissions {
/**
* 根据 pid 构建树状权限集合
*
* @param permissions 权限集合
* @param pid 权限父级 ID
* @param pid 权限父级 ID
* @return 树状权限集合
*/
public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.vo.auth;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
......@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2019/5/28 22:11
*/
@ApiModel("LoginVo")
@Schema(name = "LoginVo")
@Data
@Builder
@NoArgsConstructor
......@@ -27,9 +26,9 @@ public class LoginVo implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456896L;
@ApiModelProperty(value = "用户 ID", example = "1")
@Schema(description = "用户 ID", example = "1")
String userId;
@ApiModelProperty(value = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
@Schema(description = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
String token;
}
......@@ -4,8 +4,7 @@ package com.yiring.auth.vo.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -21,7 +20,7 @@ import lombok.experimental.FieldDefaults;
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("MenuVo")
@Schema(name = "MenuVo")
@Data
@Builder
@NoArgsConstructor
......@@ -38,24 +37,24 @@ public class MenuVo implements Serializable {
@JsonIgnore
String pid;
@ApiModelProperty(value = "唯一标识", example = "Dashboard")
@Schema(description = "唯一标识", example = "Dashboard")
String uid;
@ApiModelProperty(value = "名称", example = "Dashboard")
@Schema(description = "名称", example = "Dashboard")
String name;
@ApiModelProperty(value = "路径", example = "/dashboard")
@Schema(description = "路径", example = "/dashboard")
String path;
@ApiModelProperty(value = "重定向", example = "/dashboard/workbench")
@Schema(description = "重定向", example = "/dashboard/workbench")
String redirect;
@ApiModelProperty(value = "组件", example = "LAYOUT")
@Schema(description = "组件", example = "LAYOUT")
String component;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
@Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
@Schema(description = "子权限")
List<MenuVo> children;
}
......@@ -4,8 +4,8 @@ package com.yiring.auth.vo.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -14,13 +14,14 @@ import lombok.experimental.FieldDefaults;
/**
* 权限输出类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("PermissionVo")
@Schema(name = "PermissionVo")
@Data
@Builder
@NoArgsConstructor
......@@ -31,42 +32,42 @@ public class PermissionVo implements Serializable {
@Serial
private static final long serialVersionUID = -9139328772148985141L;
@ApiModelProperty(value = "主键", example = "1")
@Parameter(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "权限类型", example = "MENU")
@Parameter(description = "权限类型", example = "MENU")
Permission.Type type;
@ApiModelProperty(value = "序号", example = "1")
@Parameter(description = "序号", example = "1")
Integer serial;
@ApiModelProperty(value = "标识", example = "home")
@Parameter(description = "标识", example = "home")
String uid;
@ApiModelProperty(value = "名称", example = "主页")
@Parameter(description = "名称", example = "主页")
String name;
@ApiModelProperty(value = "路径", example = "/")
@Parameter(description = "路径", example = "/")
String path;
@ApiModelProperty(value = "组件", example = "/home")
@Parameter(description = "组件", example = "/home")
String component;
@ApiModelProperty(value = "图标", example = "menu")
@Parameter(description = "图标", example = "menu")
String icon;
@ApiModelProperty(value = "是否隐藏", example = "false")
@Parameter(description = "是否隐藏", example = "false")
Boolean hidden;
@ApiModelProperty(value = "是否启用", example = "true")
@Parameter(description = "是否启用", example = "true")
Boolean enable;
@ApiModelProperty(value = "父级ID", example = "0")
@Parameter(description = "父级ID", example = "0")
String pid;
@ApiModelProperty(value = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
@Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
@Parameter(description = "子权限")
List<PermissionVo> children;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.role;
import com.yiring.auth.vo.permission.PermissionVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
......@@ -12,12 +11,13 @@ import lombok.experimental.FieldDefaults;
/**
* 角色响应类
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@ApiModel("RoleVo")
@Schema(name = "RoleVo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,15 +28,15 @@ public class RoleVo implements Serializable {
@Serial
private static final long serialVersionUID = -9154497137563970840L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "标识", example = "admin")
@Schema(description = "标识", example = "admin")
String uid;
@ApiModelProperty(value = "名称", example = "系统管理员")
@Schema(description = "名称", example = "系统管理员")
String name;
@ApiModelProperty("权限")
@Schema(description = "权限")
List<PermissionVo> permissions;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
......@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserInfo")
@Schema(name = "UserInfo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,28 +28,28 @@ public class UserInfoVo implements Serializable {
@Serial
private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String userId;
@ApiModelProperty(value = "手机号", example = "15616260195")
@Schema(description = "手机号", example = "15616260195")
String mobile;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "介绍", example = "系统管理员")
@Schema(description = "介绍", example = "系统管理员")
String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
@Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar;
@ApiModelProperty("角色")
@Schema(description = "角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
@Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath;
}
......@@ -2,8 +2,7 @@
package com.yiring.auth.vo.user;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
......@@ -13,11 +12,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserInfo")
@Schema(name = "UserInfo")
@Data
@Builder
@NoArgsConstructor
......@@ -28,25 +28,25 @@ public class UserMenuListVo implements Serializable {
@Serial
private static final long serialVersionUID = -5319037883240327088L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "介绍", example = "系统管理员")
@Schema(description = "介绍", example = "系统管理员")
String desc;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
@Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar;
@ApiModelProperty("角色")
@Schema(description = "角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
@Schema(description = "用户主页", example = "/dashboard/workbench")
String homePath;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.user;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
......@@ -11,11 +10,12 @@ import lombok.experimental.FieldDefaults;
/**
* 用户信息
*
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserVo")
@Schema(name = "UserVo")
@Data
@Builder
@NoArgsConstructor
......@@ -26,39 +26,39 @@ public class UserVo implements Serializable {
@Serial
private static final long serialVersionUID = -2184378273450466835L;
@ApiModelProperty(value = "主键", example = "1")
@Schema(description = "主键", example = "1")
String id;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
@Schema(description = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
@Schema(description = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "手机号", example = "13012345678")
@Schema(description = "手机号", example = "13012345678")
String mobile;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
@Schema(description = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "职称", example = "系统管理员")
@Schema(description = "职称", example = "系统管理员")
String title;
@ApiModelProperty(value = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
@Schema(description = "头像", example = "https://s1.ax1x.com/2022/03/30/qggJH0.jpg")
String avatar;
@ApiModelProperty(value = "是否启用", example = "true")
@Schema(description = "是否启用", example = "true")
Boolean enabled;
@ApiModelProperty(value = "是否删除", example = "false")
@Schema(description = "是否删除", example = "false")
Boolean deleted;
@ApiModelProperty(value = "最后登录IP地址", example = "127.0.0.1")
@Schema(description = "最后登录IP地址", example = "127.0.0.1")
String lastLoginIp;
@ApiModelProperty(value = "最后登录时间", example = "2022-10-24 10:24:00")
@Schema(description = "最后登录时间", example = "2022-10-24 10:24:00")
LocalDateTime lastLoginTime;
@ApiModelProperty(value = "最后登录时间", example = "2022-01-01 00:00:00")
@Schema(description = "最后登录时间", example = "2022-01-01 00:00:00")
LocalDateTime createTime;
}
......@@ -5,7 +5,6 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.secure.SaSecureUtil;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.param.auth.LoginParam;
......@@ -16,12 +15,15 @@ import com.yiring.auth.vo.auth.LoginVo;
import com.yiring.common.core.Result;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.util.Commons;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import javax.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Example;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
......@@ -39,9 +41,11 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "all" })
@ApiSupport(order = -99)
@Api(tags = "身份认证", description = "Auth")
@Tag(
name = "Auth",
description = "身份认证",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9999") }) }
)
@RestController
@RequestMapping("/auth/")
@RequiredArgsConstructor
......@@ -50,9 +54,9 @@ public class AuthController {
final Auths auths;
final UserRepository userRepository;
@ApiOperation(value = "注册")
@PostMapping("register")
public Result<String> register(@Validated RegisterParam param) {
@Operation(summary = "注册")
@PostMapping(value = "register")
public Result<String> register(@ParameterObject @Validated RegisterParam param) {
// 检查用户名是否存在
long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build()));
if (count > 0) {
......@@ -88,9 +92,9 @@ public class AuthController {
return Result.ok();
}
@ApiOperation(value = "登录")
@Operation(summary = "登录")
@PostMapping("login")
public Result<LoginVo> login(@Validated LoginParam param, HttpServletRequest request) {
public Result<LoginVo> login(@ParameterObject @Validated LoginParam param, HttpServletRequest request) {
// 查询用户信息是否匹配
User user = userRepository.findByAccount(param.getAccount());
if (user == null) {
......@@ -126,13 +130,13 @@ public class AuthController {
return Result.ok(vo);
}
@ApiOperation(value = "检查登录")
@Operation(summary = "检查登录")
@GetMapping("valid")
public Result<Boolean> valid() {
return Result.ok(StpUtil.isLogin());
}
@ApiOperation(value = "登出")
@Operation(summary = "登出")
@GetMapping("logout")
public Result<String> logout() {
StpUtil.logout();
......@@ -144,13 +148,12 @@ public class AuthController {
* 默认安全时间: 120s
*
* @param param 用户密码
* @return
* @link { https://sa-token.dev33.cn/doc.html#/up/safe-auth }
* @link { <a href="https://sa-token.dev33.cn/doc.html#/up/safe-auth">...</a> }
*/
@SaCheckLogin
@ApiOperation(value = "安全校验")
@Operation(summary = "安全验证")
@GetMapping("safe")
public Result<String> safe(@Validated SafeParam param) {
public Result<String> safe(@ParameterObject @Validated SafeParam param) {
User user = auths.getLoginUser();
if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) {
StpUtil.openSafe(120);
......
......@@ -4,7 +4,6 @@ package com.yiring.auth.web.sys.permission;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONValidator;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam;
......@@ -18,13 +17,16 @@ import com.yiring.common.param.PageParam;
import com.yiring.common.param.PidParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import 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;
......@@ -44,9 +46,11 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -97)
@Api(tags = "权限管理", description = "Permission")
@Tag(
name = "权限管理",
description = "Permission",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-99") }) }
)
@RestController
@RequestMapping("/sys/permission/")
@RequiredArgsConstructor
......@@ -54,9 +58,9 @@ public class PermissionController {
final PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@Validated({ Group.Add.class }) PermissionParam param) {
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) PermissionParam param) {
if (has(param.getUid())) {
throw BusinessException.i18n("Code.1001");
}
......@@ -66,9 +70,9 @@ public class PermissionController {
return Result.ok();
}
@ApiOperation(value = "修改")
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@Validated({ Group.Edit.class }) PermissionParam param) {
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) PermissionParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -86,9 +90,9 @@ public class PermissionController {
return Result.ok();
}
@ApiOperation(value = "删除")
@Operation(summary = "删除")
@PostMapping("deleted")
public Result<String> deleted(@Validated IdParam param) {
public Result<String> deleted(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -99,9 +103,9 @@ public class PermissionController {
return Result.ok();
}
@ApiOperation(value = "查询")
@Operation(summary = "查询")
@GetMapping("find")
public Result<PermissionVo> find(@Validated IdParam param) {
public Result<PermissionVo> find(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -114,18 +118,18 @@ public class PermissionController {
return Result.ok(vo);
}
@ApiOperation(value = "分页查询")
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<PermissionVo>> page(@Validated PageParam param) {
public Result<PageVo<PermissionVo>> page(@ParameterObject @Validated PageParam param) {
Page<Permission> page = permissionRepository.findAll(PageParam.toPageable(param));
List<PermissionVo> data = Permissions.toPermissionVos(page.toList());
PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@ApiOperation(value = "树结构查询")
@Operation(summary = "树结构查询")
@GetMapping(value = "tree")
public Result<ArrayList<PermissionVo>> tree(@Validated(Group.Optional.class) PidParam param) {
public Result<ArrayList<PermissionVo>> tree(@ParameterObject @Validated(Group.Optional.class) PidParam param) {
List<Permission> permissions = permissionRepository.findAll();
List<PermissionVo> vos = Permissions.toTree(
permissions,
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.role;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.role.Role;
......@@ -17,12 +16,15 @@ import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.io.Serializable;
import java.util.*;
import 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;
......@@ -42,9 +44,11 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -96)
@Api(tags = "角色管理", description = "Role")
@Tag(
name = "角色管理",
description = "Role",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-98") }) }
)
@RestController
@RequestMapping("/sys/role/")
@RequiredArgsConstructor
......@@ -53,9 +57,9 @@ public class RoleController {
final RoleRepository roleRepository;
final PermissionRepository permissionRepository;
@ApiOperation(value = "新增")
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@Validated({ Group.Add.class }) RoleParam param) {
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) RoleParam param) {
if (has(param.getUid())) {
throw BusinessException.i18n("Code.1002");
}
......@@ -66,9 +70,9 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "修改")
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@Validated({ Group.Edit.class }) RoleParam param) {
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) RoleParam param) {
Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -87,9 +91,12 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "分配权限")
@Operation(summary = "分配权限")
@PostMapping("assign")
public Result<String> assign(@Validated IdParam idParam, @Validated IdsParam idsParam) {
public Result<String> assign(
@ParameterObject @Validated IdParam idParam,
@ParameterObject @Validated IdsParam idsParam
) {
Optional<Role> optional = roleRepository.findById(idParam.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -105,17 +112,17 @@ public class RoleController {
return Result.ok();
}
@ApiOperation(value = "删除")
@Operation(summary = "删除")
@PostMapping("deleted")
public Result<String> deleted(@Validated IdsParam param) {
public Result<String> deleted(@ParameterObject @Validated IdsParam param) {
List<Role> roles = roleRepository.findAllById(param.toIds());
roleRepository.deleteAll(roles);
return Result.ok();
}
@ApiOperation(value = "查询")
@Operation(summary = "查询")
@GetMapping("find")
public Result<RoleVo> find(@Validated IdParam param) {
public Result<RoleVo> find(@ParameterObject @Validated IdParam param) {
Optional<Role> optional = roleRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -128,16 +135,16 @@ public class RoleController {
return Result.ok(vo);
}
@ApiOperation(value = "分页查询")
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<RoleVo>> page(@Validated PageParam param) {
public Result<PageVo<RoleVo>> page(@ParameterObject @Validated PageParam param) {
Page<Role> page = roleRepository.findAll(PageParam.toPageable(param));
List<RoleVo> data = new ArrayList<>(Permissions.toRoleVos(page.toSet()));
PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@ApiOperation(value = "选项查询")
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<RoleVo>> selector() {
List<Role> roles = roleRepository.findAll();
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.sys.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User;
......@@ -14,13 +13,16 @@ import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.util.Commons;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.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;
......@@ -38,9 +40,11 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -95)
@Api(tags = "用户管理", description = "User")
@Tag(
name = "用户管理",
description = "User",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-97") }) }
)
@RestController
@RequestMapping("/sys/user/")
@RequiredArgsConstructor
......@@ -49,9 +53,12 @@ public class UserController {
final UserRepository userRepository;
final RoleRepository roleRepository;
@ApiOperation(value = "分配角色")
@Operation(summary = "分配角色")
@PostMapping("assign")
public Result<String> assign(@Validated IdParam idParam, @Validated IdsParam idsParam) {
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();
......@@ -67,9 +74,9 @@ public class UserController {
return Result.ok();
}
@ApiOperation(value = "分页查询")
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<UserVo>> page(@Validated PageParam param) {
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());
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.web.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
......@@ -9,8 +8,10 @@ import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.common.core.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
......@@ -31,9 +32,11 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -95)
@Api(tags = "用户信息", description = "UserView")
@Tag(
name = "用户信息",
description = "UserView",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9998") }) }
)
@RestController
@RequestMapping("/user/")
@RequiredArgsConstructor
......@@ -41,7 +44,7 @@ public class UserViewController {
final Auths auths;
@ApiOperation(value = "获取登录用户信息")
@Operation(summary = "获取登录用户信息")
@GetMapping("getUserInfo")
public Result<UserInfoVo> getUserInfo() {
User user = auths.getLoginUser();
......@@ -57,7 +60,7 @@ public class UserViewController {
return Result.ok(userInfoVo);
}
@ApiOperation(value = "获取用户菜单")
@Operation(summary = "获取用户菜单")
@GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() {
User user = auths.getLoginUser();
......@@ -70,7 +73,7 @@ public class UserViewController {
return Result.ok((ArrayList<MenuVo>) vos);
}
@ApiOperation(value = "获取用户权限")
@Operation(summary = "获取用户权限")
@GetMapping("getPermCode")
public Result<ArrayList<String>> getPermCode() {
User user = auths.getLoginUser();
......
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
token-name: App-Token
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
......
Code.1000=\u7528\u6237\u4E0D\u5B58\u5728
Code.1001=\u6743\u9650\u6807\u8BC6\u91CD\u590D
Code.1002=\u89D2\u8272\u6807\u8BC6\u91CD\u590D
Code.10000=\u4E8C\u7EA7\u8BA4\u8BC1\u6821\u9A8C\u5931\u8D25
Code.10001=\u89D2\u8272\u6743\u9650\u4E0D\u8DB3
Code.10002=\u6743\u9650\u4E0D\u8DB3
Code.10003=\u672A\u6388\u6743
Code.100000=\u7528\u6237\u540D\u5DF2\u5B58\u5728
Code.100001=\u624B\u673A\u53F7\u5DF2\u5B58\u5728
Code.100002=\u90AE\u7BB1\u5DF2\u5B58\u5728
......
Code.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
......
Status.OK=OK
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
......
......@@ -11,7 +11,7 @@ dependencies {
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-extra
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
......@@ -22,9 +22,9 @@ dependencies {
// JTS 几何对象操作库
implementation "org.locationtech.jts:jts-core:${jtsVersion}"
// https://github.com/vladmihalcea/hibernate-types
// hibernate-types-55
implementation "com.vladmihalcea:hibernate-types-55:${hibernateTypesVersion}"
// https://github.com/vladmihalcea/hypersistence-utils
// hypersistence-utils-hibernate-60
implementation "io.hypersistence:hypersistence-utils-hibernate-60:${hibernateTypesVersion}"
// https://mvnrepository.com/artifact/org.n52.jackson/jackson-datatype-jts/1.2.10
implementation("org.n52.jackson:jackson-datatype-jts:1.2.10") {
......
/* (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 = "string", format = "binary"),
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE
);
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.util.Commons;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
......@@ -36,9 +36,9 @@ public class RequestAspect {
Boolean debug;
/**
* 白名单
* 白名单(忽略)
*/
List<String> WHITE_LIST = List.of("/swagger-resources", "/error");
List<String> IGNORE_LIST = List.of("/swagger-resources", "/error", "/v3/api-docs");
@Pointcut(
"@annotation(org.springframework.web.bind.annotation.RequestMapping) || @annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.ExceptionHandler)"
......@@ -49,8 +49,10 @@ public class RequestAspect {
public Object around(ProceedingJoinPoint point) throws Throwable {
HttpServletRequest request = getRequest();
// 放行白名单
if (WHITE_LIST.contains(request.getServletPath())) {
return point.proceed();
for (String path : IGNORE_LIST) {
if (request.getServletPath().startsWith(path)) {
return point.proceed();
}
}
// 计算接口执行耗时
......@@ -66,10 +68,13 @@ public class RequestAspect {
String extra = "";
if (Boolean.TRUE.equals(debug)) {
String headers = JSONObject.toJSONString(
ServletUtil.getHeaderMap(request),
JakartaServletUtil.getHeaderMap(request),
JSONWriter.Feature.PrettyFormat
);
String params = JSONObject.toJSONString(
JakartaServletUtil.getParamMap(request),
JSONWriter.Feature.PrettyFormat
);
String params = JSONObject.toJSONString(ServletUtil.getParamMap(request), JSONWriter.Feature.PrettyFormat);
extra += String.format("\nHeaders: %s", headers);
extra += String.format("\nParams: %s", params);
if (result instanceof 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() {}
}
......@@ -4,7 +4,7 @@ package com.yiring.common.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.n52.jackson.datatype.jts.JtsModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -18,10 +18,10 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@RequiredArgsConstructor
public class JacksonConfig {
@Resource
JavaTimeModule javaTimeModule;
final JavaTimeModule javaTimeModule;
@Bean
public ObjectMapper objectMapper() {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.PropertyKey;
/**
......@@ -23,13 +23,11 @@ import org.jetbrains.annotations.PropertyKey;
*/
@SuppressWarnings({ "unchecked", "unused" })
@ApiModel("Result")
@JsonInclude(JsonInclude.Include.NON_NULL)
@Slf4j
@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Result<T extends Serializable> implements Serializable {
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = -4802543396830024571L;
......@@ -46,49 +44,49 @@ public class Result<T extends Serializable> implements Serializable {
/**
* 接口响应时间
*/
@ApiModelProperty(value = "响应时间", example = "2021-01-01 00:00:00")
@Schema(description = "响应时间", example = "2021-01-01 00:00:00")
String timestamp;
/**
* 接口耗时(单位:秒)通常在调试阶段出现
*/
@ApiModelProperty(value = "耗时", example = "0.001s")
@Schema(description = "耗时", example = "0.001s")
String times;
/**
* 响应状态码
*/
@ApiModelProperty(value = "状态码", example = "200")
@Schema(description = "状态码", example = "200", defaultValue = "200")
Integer status;
/**
* 业务标识码
* 响应消息
*/
@ApiModelProperty(value = "业务标识码", example = "0")
Integer code;
@Schema(description = "消息", example = "OK", defaultValue = "OK")
String message;
/**
* 响应消息
* 业务标识码
*/
@ApiModelProperty(value = "消息", example = "OK")
String message;
@Schema(description = "业务标识码", example = "0", nullable = true)
Integer code;
/**
* 详细信息,通常为参数校验结果或自定义消息
*/
@ApiModelProperty(value = "详细信息", example = "Details message")
@Schema(description = "详细信息", nullable = true)
String details;
/**
* 异常信息,通常在出现服务器错误时会出现该异常
*/
@ApiModelProperty(value = "异常信息", notes = "出现错误时会出现该字段", example = "Error message")
@Schema(description = "异常信息", nullable = true)
String error;
/**
* 响应内容
*/
@ApiModelProperty("内容")
@Schema(description = "内容")
T body;
/**
......@@ -97,7 +95,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @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(t(Status.OK.getReasonPhrase())).build();
}
......@@ -108,9 +106,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see com.yiring.common.core.Status
*/
public static <T extends Serializable> Result<T> ok(
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String body
) {
public static <T> Result<T> ok(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String body) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
......@@ -125,7 +121,7 @@ public class Result<T extends Serializable> implements Serializable {
* @param body {@link Object}
* @return Result
*/
public static <T extends Serializable> Result<T> ok(T body) {
public static <T> Result<T> ok(T body) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
......@@ -140,7 +136,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T extends Serializable> Result<T> no() {
public static <T> Result<T> no() {
return no(Status.BAD_REQUEST);
}
......@@ -150,9 +146,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T extends Serializable> Result<T> no(
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details
) {
public static <T> Result<T> no(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(Status.BAD_REQUEST, details);
}
......@@ -162,7 +156,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T extends Serializable> Result<T> no(Status status) {
public static <T> Result<T> no(Status status) {
return no(status, null, null, null);
}
......@@ -172,10 +166,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status
*/
public static <T extends Serializable> Result<T> no(
Status status,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details
) {
public static <T> Result<T> no(Status status, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details) {
return no(status, null, details, null);
}
......@@ -185,7 +176,7 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status
*/
public static <T extends Serializable> Result<T> no(Status status, Throwable error) {
public static <T> Result<T> no(Status status, Throwable error) {
return no(status, null, null, error);
}
......@@ -195,12 +186,22 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status
*/
public static <T extends Serializable> Result<T> no(
public static <T> Result<T> no(
Status status,
Integer code,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error
) {
if (Objects.isNull(code)) {
String prefix = "Code.";
if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, "");
code = Convert.toInt(codeText);
} else {
code = -1;
}
}
Result<T> result = (Result<T>) Result
.builder()
.status(status.value())
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.domain;
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import com.vladmihalcea.hibernate.type.json.JsonType;
import com.yiring.common.snowflake.SnowflakeId;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import javax.persistence.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.*;
import org.hibernate.snowflake.SnowflakeId;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 基础表抽象类
......@@ -28,12 +30,8 @@ import org.hibernate.snowflake.SnowflakeId;
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SuperBuilder(toBuilder = true)
@TypeDefs(
value = {
@TypeDef(name = "json", typeClass = JsonType.class), @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class),
}
)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BasicEntity {
@Comment("主键")
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.exception;
import cn.hutool.core.convert.Convert;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Status;
import java.io.Serial;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.PropertyKey;
......@@ -22,7 +22,6 @@ import org.jetbrains.annotations.PropertyKey;
@SuppressWarnings("unused")
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class BusinessException extends RuntimeException {
......@@ -30,33 +29,28 @@ public class BusinessException extends RuntimeException {
private static final long serialVersionUID = -4226669531686389671L;
/**
* 业务状态
* 状态码
*/
Integer code;
Status status;
/**
* 业务状态异常消息
*/
String message;
public BusinessException(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
String prefix = "Code.";
if (message.startsWith(prefix)) {
String code = message.replaceAll(".*(\\d+).*", "$1");
this.code = Convert.toInt(code);
this.message = message;
} else {
this.code = -1;
this.message = "Unknown Error";
}
}
public BusinessException(int code, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
this.code = code;
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(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);
}
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "IdParam", description = "公共的 ID 查询参数")
@Schema(name = "IdParam", description = "公共的 ID 查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -27,7 +27,7 @@ public class IdParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "id", example = "1", required = true)
@Parameter(description = "id", example = "1")
@NotBlank
String id;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -19,7 +19,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel("IdsParam")
@Schema(name = "IdsParam")
@Data
@Builder
@NoArgsConstructor
......@@ -30,7 +30,7 @@ public class IdsParam implements Serializable {
@Serial
private static final long serialVersionUID = -8379896695668632733L;
@ApiModelProperty(value = "ids 多个以逗号分割", example = "1,2", required = true)
@Parameter(description = "ids 多个以逗号分割", example = "1,2")
@NotBlank
String ids;
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2022/4/27 08:53
*/
@ApiModel(value = "KeywordParam", description = "公共的关键字查询参数")
@Schema(name = "KeywordParam", description = "公共的关键字查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -27,7 +27,7 @@ public class KeywordParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456894L;
@ApiModelProperty(value = "关键字", example = "hi")
@Parameter(description = "关键字", example = "hi")
@NotBlank
String keyword;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
......@@ -26,7 +26,7 @@ import org.springframework.data.domain.Sort;
* @author ifzm
* @version 0.1 2019/3/10 16:29
*/
@ApiModel(value = "PageParam", description = "公共的分页排序查询参数")
@Schema(name = "PageParam", description = "公共的分页排序查询参数")
@Data
@SuperBuilder
@NoArgsConstructor
......@@ -37,20 +37,22 @@ public class PageParam implements Serializable {
@Serial
private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10")
@Parameter
@Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull
@Range(min = 1, max = 100)
Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1")
@Parameter
@Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull
@Min(1)
Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id")
@Schema(description = "排序字段", defaultValue = "id", example = "id")
String sortField;
@ApiModelProperty(value = "排序方向(ASC|DESC)", example = "DESC")
@Schema(description = "排序方向(ASC|DESC)", defaultValue = "DESC", example = "DESC")
Sort.Direction sortOrder;
/**
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -16,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "PidParam", description = "公共的父级 ID 查询参数")
@Schema(name = "PidParam", description = "公共的父级 ID 查询参数")
@Data
@Builder
@NoArgsConstructor
......@@ -27,7 +27,7 @@ public class PidParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "pid", example = "0", required = true)
@Parameter(description = "pid", example = "0")
@NotBlank
String pid;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 Long 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateLongId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextId();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import java.io.Serializable;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.springframework.stereotype.Component;
/**
* 基于雪花算法的 ID 生成器
* 生成 String 类型
*
* @author ifzm
* @version 0.1
* 2020/1/14 16:18
*/
@Component
public class GenerateStringId implements IdentifierGenerator {
private Snowflake snowflake;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
return snowflake.nextIdStr();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
snowflake = IdUtil.getSnowflake();
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.snowflake;
import lombok.experimental.UtilityClass;
/**
* 雪花 ID 生成器常量
*
* @author ifzm
* @version 0.1
* 2020/8/10 15:35
*/
@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";
}
}
......@@ -4,11 +4,11 @@ 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;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* 枚举参数校验
......
/* (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;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 枚举参数校验器
......
......@@ -2,11 +2,11 @@
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 javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import lombok.Cleanup;
import lombok.experimental.UtilityClass;
......@@ -34,7 +34,7 @@ public class ValidateUtil {
@Cleanup
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Set<ConstraintViolation<T>> constraintViolations = factory.getValidator().validate(t, groups);
if (!Commons.isNullOrEmpty(constraintViolations)) {
if (Commons.notEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation.group;
import javax.validation.groups.Default;
import jakarta.validation.groups.Default;
/**
* validate group
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
/**
......@@ -16,7 +18,7 @@ import lombok.experimental.FieldDefaults;
* @version 0.1
* 2022/3/23 16:47
*/
@ApiModel(value = "DataVo", description = "公共数据响应输出")
@Schema(name = "DataVo", description = "公共数据响应输出")
@Data
@NoArgsConstructor
@AllArgsConstructor
......@@ -26,17 +28,18 @@ public class DataVo<T extends Serializable> implements Serializable {
@Serial
private static final long serialVersionUID = 2472779197432240431L;
@ApiModelProperty(value = "数据")
@Schema(description = "数据")
T data;
/**
* 通常在带有时效性的数据查询时有用途(可选参数)
*/
@ApiModelProperty(value = "数据最新时间")
@Schema(description = "数据最新时间")
LocalDateTime latest;
/**
* 构建一个 DataVo
*
* @param data 数据
* @return DataVo
*/
......@@ -47,7 +50,8 @@ public class DataVo<T extends Serializable> implements Serializable {
/**
* 构建一个 DataVo
* @param data 数据
*
* @param data 数据
* @param latest 数据最新时间
* @return DataVo
*/
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
......@@ -16,7 +15,7 @@ import lombok.experimental.FieldDefaults;
* 2022/3/24 17:29
*/
@ApiModel(value = "KeyValueVo", description = "键值对响应输出")
@Schema(name = "KeyValueVo", description = "键值对响应输出")
@Data
@Builder
@NoArgsConstructor
......@@ -27,15 +26,15 @@ public class KeyValueVo implements Serializable {
@Serial
private static final long serialVersionUID = -5238793972067296346L;
@ApiModelProperty(value = "key", example = "key")
@Schema(description = "key", example = "key")
String key;
@ApiModelProperty(value = "value", example = "value")
@Schema(description = "value", example = "value")
String value;
/**
* 扩展字段,可用于文本输出
*/
@ApiModelProperty(value = "label", example = "label")
@Schema(description = "label", example = "label")
String label;
}
......@@ -2,14 +2,16 @@
package com.yiring.common.vo;
import com.yiring.common.util.Commons;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import lombok.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.data.domain.Page;
......@@ -19,7 +21,7 @@ import org.springframework.data.domain.Page;
* @author ifzm
* @version 0.1 2019/3/10 16:29
*/
@ApiModel(value = "PageVo", description = "公共分页查询响应输出")
@Schema(name = "PageVo", description = "公共分页查询响应输出")
@Data
@NoArgsConstructor
@AllArgsConstructor
......@@ -29,16 +31,16 @@ public class PageVo<T extends Serializable> implements Serializable {
@Serial
private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "数据", required = true)
@Schema(description = "数据")
List<T> data;
@ApiModelProperty(value = "数据总数", example = "100", required = true)
@Schema(description = "数据总数", example = "100")
Long total;
/**
* 通常在带有时效性的数据查询时有用途(可选参数)
*/
@ApiModelProperty(value = "数据最新时间")
@Schema(description = "数据最新时间")
LocalDateTime latest;
/**
......
dependencies {
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-validation'
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.swagger;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.net.NetUtil;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import com.yiring.common.core.Status;
import io.swagger.annotations.Api;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import com.yiring.common.core.I18n;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.tags.Tag;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.RequestMethod;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.RequestHandler;
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
......@@ -42,9 +33,8 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Slf4j
@Profile("!prod")
@EnableSwagger2WebMvc
@Configuration
@Import(BeanValidatorPluginsConfiguration.class)
@RequiredArgsConstructor
public class SwaggerConfig implements CommandLineRunner {
@Value("${spring.application.name}")
......@@ -56,78 +46,110 @@ public class SwaggerConfig implements CommandLineRunner {
@Value("${server.servlet.context-path}")
String path;
@Resource
OpenApiExtensionResolver openApiExtensionResolver;
final I18n i18n;
@Bean
public OpenAPI openAPI() {
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 Docket any() {
return api("default", List.of(""), PathSelectors.any());
public GroupedOpenApi any() {
return api("default", List.of("com.yiring"), List.of("/**"), Collections.emptyList());
}
@Bean(name = "api.auth")
public Docket auth() {
return api("① Auth", List.of("com.yiring.auth.web"), Predicate.not(PathSelectors.ant(path + "/sys/**")));
public GroupedOpenApi auth() {
return api("① Auth", List.of("com.yiring.auth.web"), List.of("/**"), List.of("/sys/**"));
}
@Bean(name = "api.common")
public Docket common() {
return api("② 公共", List.of("com.yiring.common.web", "com.yiring.app.web.common"), PathSelectors.any());
public GroupedOpenApi common() {
return api(
"② 公共",
List.of("com.yiring.common.web", "com.yiring.app.web.common"),
List.of("/**"),
Collections.emptyList()
);
}
@Bean(name = "api.manage")
public Docket manage() {
return api("③ 系统管理", List.of("com.yiring.auth.web.sys"), PathSelectors.any());
public GroupedOpenApi manage() {
return api("③ 系统管理", List.of("com.yiring.auth.web.sys"), List.of("/**"), Collections.emptyList());
}
@Bean(name = "api.example")
public Docket example() {
return api("④ 示例", List.of("com.yiring.app.web.example"), PathSelectors.any());
public GroupedOpenApi example() {
return api("④ 示例", List.of("com.yiring.app.web.example"), List.of("/**"), Collections.emptyList());
}
private Docket api(String group, List<String> basePackages, Predicate<String> paths) {
// 扫描多个包
Predicate<RequestHandler> predicate = basePackages
.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));
@Bean
public OpenApiCustomizer sortTagCustom() {
String order = "x-order";
return api -> {
List<Tag> tags = api
.getTags()
.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);
};
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("API Doc")
.description(applicationName)
.version("1.0")
.contact(new Contact("© YiRing", "https://yiring.com", "developer@yiring.com"))
// @Bean
// public OperationCustomizer addGlobalStatusResponse() {
// return (operation, handlerMethod) -> {
// ApiResponses responses = operation.getResponses();
// for (Status status : Status.values()) {
// if (Status.OK.equals(status)) {
// continue;
// }
//
// ApiResponse response = new ApiResponse()
// .description(i18n.get(status.getReasonPhrase()))
// .$ref("Response");
// responses.addApiResponse(String.valueOf(status.value()), response);
// }
//
// 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(Operation.class))
.pathsToMatch(pathsToMatch.toArray(new String[0]))
.pathsToExclude(pathsToExclude.toArray(new String[0]))
.build();
}
/**
* 构建全局响应消息
*
* @return 所有自定义状态码消息
*/
private List<ResponseMessage> buildGlobalResponseMessage() {
return Arrays
.stream(Status.values())
.map(status -> new ResponseMessageBuilder().code(status.value()).message(status.getReasonPhrase()).build())
.collect(Collectors.toList());
}
@Override
public void run(String... args) {
String link = NetUtil
......@@ -135,6 +157,6 @@ public class SwaggerConfig implements CommandLineRunner {
.stream()
.map(host -> "> http://" + host + ":" + port + path + "/doc.html")
.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);
}
}
......@@ -7,6 +7,8 @@ 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.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
......@@ -18,6 +20,7 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
@RequiredArgsConstructor
public class I18n {
......
Status.OK=OK
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
......
......@@ -6,7 +6,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-spring-boot-starter:${knife4jVersion}"
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// minio
implementation "io.minio:minio:${minioVersion}"
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.constraints.NotEmpty;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -17,7 +16,7 @@ import lombok.experimental.FieldDefaults;
* 2022/7/5 15:29
*/
@ApiModel(value = "DownloadParam", description = "文件下载请求参数")
@Schema(name = "DownloadParam", description = "文件下载请求参数")
@Data
@Builder
@NoArgsConstructor
......@@ -28,11 +27,11 @@ public class DownloadParam implements Serializable {
@Serial
private static final long serialVersionUID = -8690942241103456899L;
@ApiModelProperty(value = "bucket", example = "public", required = true)
@Schema(name = "bucket", example = "public")
@NotEmpty(message = "存储桶不能为空")
String bucket;
@ApiModelProperty(value = "object", example = "cat.jpg", required = true)
@Schema(name = "object", example = "cat.jpg")
@NotEmpty(message = "文件对象不能为空")
String object;
}
......@@ -3,7 +3,7 @@ package com.yiring.common.web;
import cn.hutool.core.io.FileUtil;
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.Result;
import com.yiring.common.core.Status;
......@@ -13,16 +13,18 @@ import com.yiring.common.util.FileUtils;
import com.yiring.common.vo.ImageInfo;
import io.minio.GetObjectResponse;
import io.minio.StatObjectResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
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 javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
......@@ -36,9 +38,11 @@ import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@ApiSupport(order = -98)
@Api(tags = "文件管理", description = "file")
@Tag(
name = "文件管理",
description = "file",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-9997") }) }
)
@RequiredArgsConstructor
@RestController
@RequestMapping("/common/file/")
......@@ -50,9 +54,9 @@ public class MinioController {
/**
* minio 上传文件,成功返回文件 url
*/
@ApiOperation(value = "文件上传", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@PostMapping(value = "upload", headers = HttpHeaders.CONTENT_TYPE + "=" + MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@ApiParam(value = "文件", required = true) @RequestPart("file") MultipartFile file) {
@Operation(summary = "文件上传")
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) {
try {
// 获取文件名信息
String filename = file.getOriginalFilename();
......@@ -76,13 +80,13 @@ public class MinioController {
}
}
@ApiOperation(value = "Base64 图片上传")
@ApiImplicitParam(
@Operation(summary = "Base64 图片上传")
@Parameter(
name = "base64Image",
value = "Base64 图片信息",
description = "Base64 图片信息",
example = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAALCAYAAABYpyyrAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAATSURBVBhXYzAwNP6PjIeKgPF/ABj+RUX4hZfVAAAAAElFTkSuQmCC",
required = true,
paramType = "query"
in = ParameterIn.QUERY
)
@PostMapping(value = "uploadBase64Image")
public Result<String> uploadBase64Image(@NotBlank(message = "图片 Base64 信息不能为空") String base64Image) {
......@@ -112,9 +116,10 @@ public class MinioController {
* @param response HttpServletResponse
* @param param 请求参数
*/
@ApiOperation(value = "文件下载", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@GetMapping("download")
public void download(HttpServletResponse response, @Validated DownloadParam 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());
......
......@@ -3,7 +3,7 @@ package com.yiring.common.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -25,10 +25,10 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableCaching
@Configuration
@RequiredArgsConstructor
public class RedisConfig {
@Resource
ObjectMapper objectMapper;
final ObjectMapper objectMapper;
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
......
......@@ -3,7 +3,7 @@ package com.yiring.common.core;
import java.util.*;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
......@@ -18,10 +18,10 @@ import org.springframework.stereotype.Component;
@SuppressWarnings({ "unused" })
@Slf4j
@Component
@RequiredArgsConstructor
public final class Redis {
@Resource
RedisTemplate<String, Object> redisTemplate;
final RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
......@@ -83,7 +83,7 @@ public final class Redis {
/**
* 普通缓存获取
*
* @param key 键
* @param key
* @param type 类型
* @return 值
*/
......@@ -459,6 +459,7 @@ public final class Redis {
/**
* 获取所有 key
*
* @param pattern 查询规则
* @return 所有匹配的 Key 集合
*/
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.util;
import jakarta.servlet.http.HttpServletRequest;
import java.lang.reflect.Constructor;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
......@@ -21,7 +21,9 @@ import org.springframework.beans.BeanUtils;
@UtilityClass
public class Commons {
/** 代理 IP 请求头 */
/**
* 代理 IP 请求头
*/
private static final String[] HEADERS_TO_TRY = {
"X-Forwarded-For",
"Proxy-Client-IP",
......@@ -69,8 +71,8 @@ public class Commons {
* @param collection 集合
* @return 是否为空
*/
public boolean isNullOrEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
public boolean notEmpty(Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
/**
......@@ -85,11 +87,12 @@ public class Commons {
/**
* 对象 Copy
* @param source 源对象
* @param type 目标类型
*
* @param source 源对象
* @param type 目标类型
* @param ignoreProperties 忽略属性
* @param <T> 目标类型
* @return 目标对象
* @param <T> 目标类型
*/
public <T> T transform(Object source, Class<T> type, String... ignoreProperties) {
try {
......@@ -106,11 +109,12 @@ public class Commons {
/**
* 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合
* @param type 目标类型
*
* @param list 原始数据集合
* @param type 目标类型
* @param ignoreProperties 忽略属性
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @return 目标集合
*/
public <T, S> List<T> transform(@NonNull List<S> list, Class<T> type, String... ignoreProperties) {
......@@ -119,12 +123,13 @@ public class Commons {
/**
* 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合
* @param type 目标类型
* @param fn 自定义处理函数
*
* @param list 原始数据集合
* @param type 目标类型
* @param fn 自定义处理函数
* @param ignoreProperties 忽略属性
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @return 目标集合
*/
public <T, S> List<T> transform(
......@@ -162,6 +167,7 @@ public class Commons {
public interface CallbackFunction<S, T> {
/**
* 执行方法
*
* @param s 源对象
* @param t 目标对象
*/
......
......@@ -5,6 +5,7 @@ import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import com.yiring.common.vo.ImageInfo;
import jakarta.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
......@@ -15,7 +16,6 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.tomcat.util.http.fileupload.IOUtils;
......
plugins {
id 'java'
// https://start.spring.io
id 'org.springframework.boot' version '2.7.7'
id 'org.springframework.boot' version '3.0.1'
id 'org.graalvm.buildtools.native' version '0.9.18'
// https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id 'io.spring.dependency-management' version '1.1.0'
// https://plugins.gradle.org/plugin/com.diffplug.spotless
id "com.diffplug.spotless" version "6.3.0"
id "com.diffplug.spotless" version "6.12.1"
}
ext {
// Spotless
// https://www.npmjs.com/package/prettier
prettierVersion = '2.8.1'
prettierVersion = '2.8.2'
// https://www.npmjs.com/package/prettier-plugin-java
prettierJavaVersion = '2.0.0'
// SpringCloud
// https://start.spring.io/
springCloudVersion = '2021.0.4'
springCloudVersion = '2022.0.0'
// Dependencies
// https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-boot-starter
knife4jVersion = '2.0.9'
// // https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
knife4jOpen3Version = '4.0.0'
// https://mvnrepository.com/artifact/io.swagger/swagger-annotations
swaggerAnnotationsVersion = '1.6.9'
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot-starter
saTokenVersion = '1.33.0'
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter
saTokenVersion = '1.34.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.11'
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
......@@ -35,11 +36,11 @@ ext {
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
okhttpVersion = '4.10.0'
// https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.4.6'
// https://mvnrepository.com/artifact/com.vladmihalcea/hibernate-types-55
hibernateTypesVersion = '2.21.1'
minioVersion = '8.5.1'
// https://mvnrepository.com/artifact/io.hypersistence/hypersistence-utils-hibernate-60
hibernateTypesVersion = '3.0.1'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial
hibernateSpatialVersion = '5.6.14.Final'
hibernateSpatialVersion = '6.1.6.Final'
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
jtsVersion = '1.19.0'
// https://mvnrepository.com/artifact/com.github.liaochong/myexcel
......@@ -55,6 +56,7 @@ allprojects {
mavenLocal()
// Nexus
// 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' }
mavenCentral()
}
......
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: 自定义',
},
}
......@@ -4,7 +4,7 @@
<!-- prettier-ignore -->
- [SpringBoot v2.7.x](https://spring.io/projects/spring-boot)
- [SpringBoot v3.0.x](https://spring.io/projects/spring-boot)
- [Lombok](https://projectlombok.org/)
- [Spring Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html)
- [Spring Data Jpa](https://spring.io/projects/spring-data-jpa)
......
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
{
"name": "basic-api-project",
"name": "basic-api-boot",
"version": "0.0.1",
"scripts": {
"log": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"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",
"conventional-changelog-cli": "^2.2.2"
"cz-customizable": "^7.0.0",
"cz-git": "^1.4.1"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"path": "node_modules/cz-git"
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论