提交 97fa2e0f 作者: 方治民

feat: I18n 模块完整实现、依赖升级、细节优化

上级 be944c68
......@@ -7,8 +7,8 @@ bootJar {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 💬 Mock/Test Env
runtimeOnly 'com.h2database:h2'
// 💬 Prod/Dev Env
......
......@@ -13,10 +13,12 @@ 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.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;
......@@ -49,10 +51,10 @@ public class GlobalExceptionHandler {
value = { BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
)
public Result<String> bindErrorHandler(Exception e) {
String error = null;
String details = null;
if (e instanceof ConstraintViolationException) {
error = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
details = ((ConstraintViolationException) e).getConstraintViolations().iterator().next().getMessage();
} else {
BindingResult result = null;
if (e instanceof MethodArgumentNotValidException) {
......@@ -62,12 +64,26 @@ public class GlobalExceptionHandler {
}
if (result != null) {
ObjectError next = result.getAllErrors().iterator().next();
error = i18n.get(next);
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.startsWith("{") && template.endsWith("}")) {
prefix = fieldError.getField() + " ";
}
details = prefix + i18n.get(fieldError);
} else {
details = i18n.get(error);
}
}
}
return Result.no(Status.EXPECTATION_FAILED, error);
return Result.no(Status.EXPECTATION_FAILED, details);
}
/**
......@@ -105,7 +121,7 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(FailStatusException.class)
public Result<String> failStatusExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus(), i18n.get(e.getMessage(), e.getStatus().getReasonPhrase()));
return Result.no(e.getStatus(), e.getMessage());
}
/**
......
......@@ -2,11 +2,13 @@
package com.yiring.app.constant;
import com.yiring.app.exception.CodeException;
import com.yiring.common.core.I18n;
import io.swagger.annotations.ApiModel;
import org.jetbrains.annotations.PropertyKey;
/**
* 业务状态码(TODO: 结合 spring message i18n)
* eg: <code>throw new CodeException(Code.FAIL)</code>
* 业务状态码
* eg: <code>throw Code.$100000.exception()</code>
*
* @author Jim
* @version 0.1
......@@ -16,18 +18,24 @@ import io.swagger.annotations.ApiModel;
@SuppressWarnings({ "unused" })
@ApiModel("业务状态码")
public enum Code {
SUCCESS(0, "成功"),
SUCCESS(0, "Code.0"),
// =============================================================
// TODO: 扩展业务状态
// eg:
// 10001: 用户被禁止登录
// 100001: 用户被禁止登录
FAIL(10000, "测试错误");
$100000(100000, "Code.100000"),
$100001(100001, "Code.100001"),
$100002(100002, "Code.100002")
// =============================================================
;
private final int value;
private final String reasonPhrase;
Code(int value, String reasonPhrase) {
Code(int value, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
......
......@@ -49,6 +49,9 @@ public class UserExtension extends BasicEntity implements Serializable {
@Comment("年龄")
Integer age;
@Comment("简介")
String introduction;
public UserExtension(User user) {
this.user = user;
}
......
......@@ -122,9 +122,9 @@ public class UploadProcessServiceImpl implements UploadProcessService {
int flag = 0;
Frame frame = null;
while (flag <= ftp) {
//获取帧
// 获取帧
frame = grabber.grabImage();
//过滤前3帧,避免出现全黑图片
// 过滤前3帧,避免出现全黑图片
if ((flag > 3) && (frame != null)) {
break;
}
......
......@@ -10,6 +10,7 @@ import com.yiring.app.vo.user.UserExtensionVo;
import com.yiring.auth.annotation.AuthIgnore;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.param.PageParam;
......@@ -35,6 +36,7 @@ import org.springframework.web.bind.annotation.RestController;
/**
* 示例接口
*
* @author Jim
*/
......@@ -48,14 +50,13 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
public class ExampleController {
final I18n i18n;
final Auths auths;
final UserExtensionRepository userExtensionRepository;
String text = "😎 Hello World";
@GetMapping
public Result<String> hello() {
return Result.ok(text);
return Result.ok("example.hello");
}
/**
......@@ -63,7 +64,7 @@ public class ExampleController {
*/
@GetMapping("fail")
public Result<String> fail() {
throw Code.FAIL.exception();
throw Code.$100000.exception();
}
@SaCheckLogin
......@@ -71,6 +72,7 @@ public class ExampleController {
public Result<PageVo<String>> page(@Validated PageParam pageParam) {
log.info("PageParam: {}", pageParam);
String text = i18n.get("example.hello");
List<String> data = Arrays.asList(text.split(" "));
PageVo<String> vo = PageVo.build(data, data.size());
return Result.ok(vo);
......
......@@ -8,12 +8,13 @@ server:
spring:
application:
name: "basic-api-app"
messages:
basename: i18n/messages
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
messages:
basename: i18n/status,i18n/code,i18n/messages
always-use-message-format: true
profiles:
include: auth, conf-patch
active: dev-postgresql
......
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.0=OK
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
common.result.success = \u6210\u529F
upload.filename.null = \u4E0A\u4F20\u6587\u4EF6\u6CA1\u6709\u6587\u4EF6\u540D
NotEmpty.downloadParam.object = \u6587\u4EF6\u5BF9\u8C61\u4E0D\u80FD\u4E3A\u7A7A
# \u81EA\u5B9A\u4E49\u63D0\u793A\u6D88\u606F
# example.hello=\uD83D\uDE0E Hello World
example.hello=\uD83D\uDE0E Hello World
# \u4E1A\u52A1\u72B6\u6001\u7801\u9519\u8BEF\u6D88\u606F
Code.0=OK
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
org.hibernate.validator.constraints.CreditCardNumber.message=\u4E0D\u5408\u6CD5\u7684\u4FE1\u7528\u5361\u53F7\u7801
org.hibernate.validator.constraints.Currency.message=\u4E0D\u5408\u6CD5\u7684\u8D27\u5E01 (\u5FC5\u987B\u662F{value}\u5176\u4E2D\u4E4B\u4E00)
org.hibernate.validator.constraints.EAN.message=\u4E0D\u5408\u6CD5\u7684{type}\u6761\u5F62\u7801
org.hibernate.validator.constraints.Email.message=\u4E0D\u662F\u4E00\u4E2A\u5408\u6CD5\u7684\u7535\u5B50\u90AE\u4EF6\u5730\u5740
org.hibernate.validator.constraints.Length.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.CodePointLength.message=\u957F\u5EA6\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.LuhnCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, Luhn\u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod10Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2110\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.Mod11Check.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, \u6A2111\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.ModCheck.message=${validatedValue}\u7684\u6821\u9A8C\u7801\u4E0D\u5408\u6CD5, {modType}\u6821\u9A8C\u548C\u4E0D\u5339\u914D
org.hibernate.validator.constraints.NotBlank.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.NotEmpty.message=\u4E0D\u80FD\u4E3A\u7A7A
org.hibernate.validator.constraints.ParametersScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.Range.message=\u9700\u8981\u5728{min}\u548C{max}\u4E4B\u95F4
org.hibernate.validator.constraints.ScriptAssert.message=\u6267\u884C\u811A\u672C\u8868\u8FBE\u5F0F"{script}"\u6CA1\u6709\u8FD4\u56DE\u671F\u671B\u7ED3\u679C
org.hibernate.validator.constraints.URL.message=\u9700\u8981\u662F\u4E00\u4E2A\u5408\u6CD5\u7684URL
org.hibernate.validator.constraints.time.DurationMax.message=\u5FC5\u987B\u5C0F\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
org.hibernate.validator.constraints.time.DurationMin.message=\u5FC5\u987B\u5927\u4E8E${inclusive == true ? '\u6216\u7B49\u4E8E' : ''}${days == 0 ? '' : days += '\u5929'}${hours == 0 ? '' : hours += '\u5C0F\u65F6'}${minutes == 0 ? '' : minutes += '\u5206\u949F'}${seconds == 0 ? '' : seconds += '\u79D2'}${millis == 0 ? '' : millis += '\u6BEB\u79D2'}${nanos == 0 ? '' : nanos += '\u7EB3\u79D2'}
......@@ -57,6 +57,9 @@ public class User extends BasicEntity implements Serializable {
@Serial
private static final long serialVersionUID = -5787847701210907511L;
@Comment("头像")
String avatar;
@Comment("真实姓名")
String realName;
......@@ -72,12 +75,6 @@ public class User extends BasicEntity implements Serializable {
@Comment("密码")
String password;
@Comment("简介")
String introduction;
@Comment("头像")
String avatar;
@Comment("是否启用")
Boolean enabled;
......
......@@ -37,7 +37,7 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@SuppressWarnings({ "deprecation" })
@SuppressWarnings({ "deprecation", "all" })
@ApiSupport(order = -99)
@Api(tags = "身份认证", description = "Auth")
@RestController
......@@ -53,27 +53,26 @@ public class AuthController {
// 检查用户名是否存在
long count = userRepository.count(Example.of(User.builder().username(param.getUsername()).build()));
if (count > 0) {
return Result.no(Status.BAD_REQUEST, "用户名已存在");
throw Status.BAD_REQUEST.exception("Code.100000");
}
// 检查手机号是否存在
count = userRepository.count(Example.of(User.builder().mobile(param.getMobile()).build()));
if (count > 0) {
return Result.no(Status.BAD_REQUEST, "手机号已存在");
throw Status.BAD_REQUEST.exception("Code.100001");
}
// 检查邮箱是否存在
if (StrUtil.isNotBlank(param.getEmail())) {
count = userRepository.count(Example.of(User.builder().email(param.getEmail()).build()));
if (count > 0) {
return Result.no(Status.BAD_REQUEST, "邮箱已存在");
throw Status.BAD_REQUEST.exception("Code.100002");
}
}
// 构建用户信息写入数据库
User user = User
.builder()
.introduction(param.getIntroduction())
.avatar(param.getAvatar())
.mobile(param.getMobile())
.realName(param.getRealName())
......@@ -89,38 +88,36 @@ public class AuthController {
@ApiOperation(value = "登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid LoginParam param, HttpServletRequest request) {
String details = "账号密码错误";
// 查询用户信息是否匹配
User user = userRepository.findByAccount(param.getAccount());
if (user == null) {
return Result.no(Status.BAD_REQUEST, details);
throw Status.BAD_REQUEST.exception("Code.100003");
}
// 检查密码
String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) {
return Result.no(Status.BAD_REQUEST, details);
throw Status.BAD_REQUEST.exception("Code.100003");
}
// 检查用户是否已被删除
if (user.getDeleteTime() != null) {
return Result.no(Status.FORBIDDEN, "用户被禁用, 请联系管理员");
throw Status.BAD_REQUEST.exception("Code.100004");
}
// 检查用户是否被允许登录
if (!Boolean.TRUE.equals(user.getEnabled())) {
return Result.no(Status.FORBIDDEN, "用户被禁止登录, 请联系管理员");
throw Status.BAD_REQUEST.exception("Code.100005");
}
// 登录
StpUtil.login(user.getId());
// 更新用户信息
user.setLastLoginIp(Commons.getClientIpAddress(request));
user.setLastLoginTime(LocalDateTime.now());
userRepository.saveAndFlush(user);
// 登录
StpUtil.login(user.getId());
// 构建用户所需信息
LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build();
return Result.ok(vo);
......
......@@ -52,7 +52,6 @@ public class UserViewController {
.username(user.getUsername())
.realName(user.getRealName())
.avatar(user.getAvatar())
.desc(user.getIntroduction())
.roles(Permissions.toRoleVos(user.getRoles()))
.build();
return Result.ok(userInfoVo);
......
dependencies {
implementation project(":basic-common:util")
implementation project(":basic-common:i18n")
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 本地依赖
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
......@@ -31,4 +32,6 @@ dependencies {
exclude group: 'org.locationtech.jts'
}
// https://mvnrepository.com/artifact/org.jetbrains/annotations
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.core;
import cn.hutool.extra.spring.SpringUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
......@@ -11,6 +12,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.PropertyKey;
/**
* 标准的响应对象(所有的接口响应内容格式都应该是一致的)
......@@ -33,6 +35,15 @@ public class Result<T extends Serializable> implements Serializable {
private static final long serialVersionUID = -4802543396830024571L;
/**
* 注入 I18n
*/
protected static final I18n i18n;
static {
i18n = SpringUtil.getBean(I18n.class);
}
/**
* 接口响应时间
*/
@ApiModelProperty(value = "响应时间", example = "2021-01-01 00:00:00")
......@@ -87,7 +98,25 @@ public class Result<T extends Serializable> implements Serializable {
* @see com.yiring.common.core.Status
*/
public static <T extends Serializable> Result<T> ok() {
return (Result<T>) Result.builder().status(Status.OK.value()).message(Status.OK.getReasonPhrase()).build();
return (Result<T>) Result.builder().status(Status.OK.value()).message(t(Status.OK.getReasonPhrase())).build();
}
/**
* 返回成功响应内容
*
* @param body {@link String} {@link I18n}
* @return Result
* @see com.yiring.common.core.Status
*/
public static <T extends Serializable> Result<T> ok(
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String body
) {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
.message(t(Status.OK.getReasonPhrase()))
.body(t(body))
.build();
}
/**
......@@ -100,7 +129,7 @@ public class Result<T extends Serializable> implements Serializable {
return (Result<T>) Result
.builder()
.status(Status.OK.value())
.message(Status.OK.getReasonPhrase())
.message(t(Status.OK.getReasonPhrase()))
.body(body)
.build();
}
......@@ -121,7 +150,9 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status#BAD_REQUEST
*/
public static <T extends Serializable> Result<T> no(String details) {
public static <T extends Serializable> Result<T> no(
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details
) {
return no(Status.BAD_REQUEST, details);
}
......@@ -141,7 +172,10 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status
*/
public static <T extends Serializable> Result<T> no(Status status, String details) {
public static <T extends Serializable> Result<T> no(
Status status,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details
) {
return no(status, null, details, null);
}
......@@ -161,13 +195,18 @@ public class Result<T extends Serializable> implements Serializable {
* @return Result
* @see Status
*/
public static <T extends Serializable> Result<T> no(Status status, Integer code, String details, Throwable error) {
public static <T extends Serializable> Result<T> no(
Status status,
Integer code,
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error
) {
Result<T> result = (Result<T>) Result
.builder()
.status(status.value())
.message(status.getReasonPhrase())
.message(t(status.getReasonPhrase()))
.code(code)
.details(details)
.details(t(details))
.build();
if (error != null) {
......@@ -176,4 +215,18 @@ public class Result<T extends Serializable> implements Serializable {
return result;
}
/**
* i18n 默认值获取简单包装
*
* @param message 文本消息
* @return i18n 翻译结果文本消息
*/
public static String t(String message) {
if (message == null) {
return null;
}
return i18n.get(message, message);
}
}
......@@ -2,11 +2,13 @@
package com.yiring.common.core;
import com.yiring.common.exception.FailStatusException;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.lang.Nullable;
/**
* API 响应状态码
* 包含系统和业务两个维度
*
* @author ifzm
*/
......@@ -15,73 +17,73 @@ public enum Status {
/**
* 成功
*/
OK(200, "OK"),
OK(200, "Status.OK"),
/**
* 用户认证失败
*/
NON_AUTHORITATIVE_INFORMATION(203, "认证失败"),
NON_AUTHORITATIVE_INFORMATION(203, "Status.NON_AUTHORITATIVE_INFORMATION"),
/**
* 失败的请求,通常是一些验证错误
*/
BAD_REQUEST(400, "FAIL"),
BAD_REQUEST(400, "Status.BAD_REQUEST"),
/**
* 鉴权失败
*/
UNAUTHORIZED(401, "凭证过期"),
UNAUTHORIZED(401, "Status.UNAUTHORIZED"),
/**
* Token 错误/失效
*/
FORBIDDEN(403, "禁止访问"),
FORBIDDEN(403, "Status.FORBIDDEN"),
/**
* 找不到资源
*/
NOT_FOUND(404, "Not Found"),
NOT_FOUND(404, "Status.NOT_FOUND"),
/**
* 不支持的请求类型
*/
METHOD_NOT_ALLOWED(405, "不支持的请求类型"),
METHOD_NOT_ALLOWED(405, "Status.METHOD_NOT_ALLOWED"),
/**
* 参数校验失败
*/
EXPECTATION_FAILED(417, "无效参数"),
EXPECTATION_FAILED(417, "Status.EXPECTATION_FAILED"),
/**
* 服务器错误
*/
INTERNAL_SERVER_ERROR(500, "服务器错误"),
INTERNAL_SERVER_ERROR(500, "Status.INTERNAL_SERVER_ERROR"),
/**
* 未知错误
*/
UNKNOWN_ERROR(500, "未知错误"),
UNKNOWN_ERROR(500, "Status.UNKNOWN_ERROR"),
/**
* API 未实现
*/
NOT_IMPLEMENTED(501, "API 未实现"),
NOT_IMPLEMENTED(501, "Status.NOT_IMPLEMENTED"),
/**
* 服务异常(网关提醒)
*/
BAD_GATEWAY(502, "Bad Gateway"),
BAD_GATEWAY(502, "Status.BAD_GATEWAY"),
/**
* 服务暂停(网关提醒)
*/
SERVICE_UNAVAILABLE(503, "Service Unavailable");
SERVICE_UNAVAILABLE(503, "Status.SERVICE_UNAVAILABLE");
private final int value;
private final String reasonPhrase;
Status(int value, String reasonPhrase) {
Status(int value, @PropertyKey(resourceBundle = "i18n.status") String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}
......@@ -152,15 +154,16 @@ public enum Status {
*
* @param message 异常消息
*/
public FailStatusException exception(String message) {
public FailStatusException exception(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) {
return new FailStatusException(this, message);
}
/**
* 暴露异常
*
* @param message 异常消息
*/
public void expose(String message) throws FailStatusException {
public void expose(@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String message) throws FailStatusException {
throw exception(message);
}
}
......@@ -6,7 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -30,6 +30,6 @@ public class IdParam implements Serializable {
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "id", example = "1", required = true)
@NotNull(message = "id 不能为空")
@NotBlank
String id;
}
......@@ -9,7 +9,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -33,7 +33,7 @@ public class IdsParam implements Serializable {
private static final long serialVersionUID = -8379896695668632733L;
@ApiModelProperty(value = "ids 多个以逗号分割", example = "1,2", required = true)
@NotEmpty(message = "ids 不能为空")
@NotBlank
String ids;
/**
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 公共的 Boolean 查询参数
*
* @author ifzm
* @version 0.1
* 2019/5/28 22:11
*/
@ApiModel(value = "BooleanParam", description = "公共的 Boolean 查询参数")
@Valid
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class OptionalBooleanParam implements Serializable {
@Serial
private static final long serialVersionUID = -3100195332181882287L;
@ApiModelProperty(value = "value", example = "true")
Boolean value;
}
......@@ -6,13 +6,14 @@ import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Min;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
......@@ -35,11 +36,11 @@ public class OptionalPageParam implements Serializable {
private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10", required = true)
@DecimalMin(value = "1", message = "分页条数不能小于1")
@Range(min = 1, max = 100)
Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1", required = true)
@DecimalMin(value = "1", message = "当前页数不能小于1")
@Min(1)
Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id")
......
......@@ -6,7 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
......@@ -14,6 +14,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
import org.hibernate.validator.constraints.Range;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
......@@ -36,13 +37,13 @@ public class PageParam implements Serializable {
private static final long serialVersionUID = 6103761701912769946L;
@ApiModelProperty(value = "分页条数", example = "10", required = true)
@NotNull(message = "分页条数不能为空")
@DecimalMin(value = "1", message = "分页条数不能小于1")
@NotNull
@Range(min = 1, max = 100)
Integer pageSize;
@ApiModelProperty(value = "当前页数", example = "1", required = true)
@NotNull(message = "当前页数不能为空")
@DecimalMin(value = "1", message = "当前页数不能小于1")
@NotNull
@Min(1)
Integer pageNo;
@ApiModelProperty(value = "排序字段", example = "id")
......@@ -63,7 +64,7 @@ public class PageParam implements Serializable {
return Pageable.unpaged();
}
return PageParam.toPageable(param.getSortField(), param.getSortOrder(), param.pageSize, param.getPageNo());
return PageParam.toPageable(param.getSortField(), param.getSortOrder(), param.getPageSize(), param.getPageNo());
}
/**
......@@ -71,8 +72,8 @@ public class PageParam implements Serializable {
*
* @param sortField 排序字段
* @param sortOrder 排序方向
* @param pageSize 分页大小
* @param pageNo 分页页码
* @param pageSize 分页大小
* @param pageNo 分页页码
* @return Pageable
*/
public static Pageable toPageable(String sortField, Sort.Direction sortOrder, Integer pageSize, Integer pageNo) {
......
......@@ -6,7 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import lombok.*;
import lombok.experimental.FieldDefaults;
......@@ -30,6 +30,6 @@ public class PidParam implements Serializable {
private static final long serialVersionUID = -8690942241103456893L;
@ApiModelProperty(value = "pid", example = "0", required = true)
@NotNull(message = "pid 不能为空")
@NotBlank
String pid;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
/**
* 枚举参数校验
*
* @author Jim
* @version 0.1
* 2022/9/29 16:04
*/
@Documented
@Retention(value = RUNTIME)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Constraint(validatedBy = { EnumValueValidated.class })
public @interface EnumValue {
/**
* 是否需要(true:不能为空,false:可以为空)
*/
boolean isRequire() default false;
/**
* 字符串数组
*/
String[] strValues() default {};
/**
* int数组
*/
int[] intValues() default {};
/**
* 枚举类
*/
Class<?>[] enumClass() default {};
String message() default "所传参数不在允许的值范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.validation;
import 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;
/**
* 枚举参数校验器
*
* @author Jim
* @version 0.1
* 2022/9/29 16:06
*/
public class EnumValueValidated implements ConstraintValidator<EnumValue, Object> {
private boolean isRequire;
private Set<String> strValues;
private List<Integer> intValues;
@Override
public void initialize(EnumValue constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
strValues = Set.of(constraintAnnotation.strValues());
intValues = Arrays.stream(constraintAnnotation.intValues()).boxed().collect(Collectors.toList());
isRequire = constraintAnnotation.isRequire();
// 将枚举类的 name 转小写存入 strValues 里面,作为校验参数
Optional
.ofNullable(constraintAnnotation.enumClass())
.ifPresent(e ->
Arrays
.stream(e)
.forEach(c ->
Arrays.stream(c.getEnumConstants()).forEach(v -> strValues.add(v.toString().toLowerCase()))
)
);
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null && !isRequire) {
return true;
}
if (value instanceof String) {
return strValues.contains(value);
}
if (value instanceof Integer) {
return intValues.stream().anyMatch(e -> e.equals(value));
}
return false;
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation "org.jetbrains:annotations:${jetbrainsAnnotationsVersion}"
}
......@@ -2,8 +2,11 @@
package com.yiring.common.config;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
......@@ -16,6 +19,7 @@ import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
*/
@Configuration
@RequiredArgsConstructor
public class I18nConfig {
@Bean
......@@ -25,4 +29,16 @@ public class I18nConfig {
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
@Bean
public LocalValidatorFactoryBean getValidator() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation");
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
}
}
......@@ -3,6 +3,7 @@ package com.yiring.common.core;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.PropertyKey;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;
......@@ -21,10 +22,13 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class I18n {
public static final String RESOURCE_BUNDLE = "i18n.messages";
final MessageSource messageSource;
/**
* 根据 MessageSourceResolvable 获取 I18n 消息
*
* @param resolvable MessageSourceResolvable
* @return 消息内容
*/
......@@ -36,32 +40,38 @@ public class I18n {
/**
* 根据 code 和注入参数获取 I18n 消息
* eg:
* default.nonnull = {0}不能为空
* message.username.not-empty = 用户姓名不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty")
* @param code 消息标识
* default.nonnull = {0}不能为空
* message.username.not-empty = 用户姓名不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty")
*
* @param key 消息标识
* @param args 注入参数
* @return 消息内容
*/
@SuppressWarnings("unused")
public String get(String code, Object... args) {
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
public String get(@PropertyKey(resourceBundle = RESOURCE_BUNDLE) String key, Object... args) {
return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());
}
/**
* 根据 code 和注入参数获取 I18n 消息
* eg:
* default.nonnull = {0}不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty", "用户姓名不能为空")
* @param code 消息标识
* default.nonnull = {0}不能为空
* I18n.get("default.nonnull", "用户姓名")
* I18n.get("message.username.not-empty", "用户姓名不能为空")
*
* @param key 消息标识
* @param defaultMessage 默认消息
* @param args 注入参数
* @param args 注入参数
* @return 消息内容
*/
@SuppressWarnings("unused")
public String get(String code, String defaultMessage, Object... args) {
return messageSource.getMessage(code, args, defaultMessage, LocaleContextHolder.getLocale());
public String get(
@PropertyKey(resourceBundle = RESOURCE_BUNDLE) String key,
String defaultMessage,
Object... args
) {
return messageSource.getMessage(key, args, defaultMessage, LocaleContextHolder.getLocale());
}
}
Status.OK=OK
Status.NON_AUTHORITATIVE_INFORMATION=Non-Authoritative Information
Status.BAD_REQUEST=Bad Request
Status.UNAUTHORIZED=Unauthorized
Status.FORBIDDEN=Forbidden
Status.NOT_FOUND=Not Found
Status.METHOD_NOT_ALLOWED=Method Not Allowed
Status.EXPECTATION_FAILED=Expectation Failed
Status.INTERNAL_SERVER_ERROR=Internal Server Error
Status.UNKNOWN_ERROR=Unknown Error
Status.NOT_IMPLEMENTED=Not Implemented
Status.BAD_GATEWAY=Bad Gateway
Status.SERVICE_UNAVAILABLE=Service Unavailable
Status.OK=OK
Status.NON_AUTHORITATIVE_INFORMATION=\u8BA4\u8BC1\u5931\u8D25
Status.BAD_REQUEST=\u8BF7\u6C42\u5931\u8D25
Status.UNAUTHORIZED=\u51ED\u8BC1\u8FC7\u671F
Status.FORBIDDEN=\u7981\u6B62\u8BBF\u95EE
Status.NOT_FOUND=\u627E\u4E0D\u5230\u8D44\u6E90
Status.METHOD_NOT_ALLOWED=\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u7C7B\u578B
Status.EXPECTATION_FAILED=\u65E0\u6548\u53C2\u6570
Status.INTERNAL_SERVER_ERROR=\u670D\u52A1\u5668\u9519\u8BEF
Status.UNKNOWN_ERROR=\u672A\u77E5\u9519\u8BEF
Status.NOT_IMPLEMENTED=API \u672A\u5B9E\u73B0
Status.BAD_GATEWAY=\u670D\u52A1\u5F02\u5E38
Status.SERVICE_UNAVAILABLE=\u670D\u52A1\u6682\u505C
......@@ -35,6 +35,6 @@ public class DownloadParam implements Serializable {
String bucket;
@ApiModelProperty(value = "object", example = "cat.jpg", required = true)
@NotEmpty
@NotEmpty(message = "文件对象不能为空")
String object;
}
......@@ -34,8 +34,9 @@ public class FileUtils {
/**
* 文件下载
*
* @param response HttpServletResponse
* @param file File
* @param file File
* @throws IOException IOException
*/
public void download(HttpServletResponse response, File file) throws IOException {
......@@ -51,11 +52,12 @@ public class FileUtils {
/**
* 文件下载
* @param response HttpServletResponse
* @param object 文件流
* @param length 文件大小
* @param filename 文件名称(带后缀)
* @param contentType 文档类型
*
* @param response HttpServletResponse
* @param object 文件流
* @param length 文件大小
* @param filename 文件名称(带后缀)
* @param contentType 文档类型
* @param lastModified 最后修改时间
* @throws IOException IOException
*/
......@@ -74,6 +76,7 @@ public class FileUtils {
response.setContentType(contentType);
IOUtils.copy(object, response.getOutputStream());
object.close();
response.flushBuffer();
}
/**
......
......@@ -27,7 +27,7 @@ ext {
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot-starter
saTokenVersion = '1.31.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.7'
hutoolVersion = '5.8.8'
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
fastJsonVersion = '2.0.14'
// https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core
......@@ -35,15 +35,17 @@ ext {
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
okhttpVersion = '4.10.0'
// https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.4.4'
minioVersion = '8.4.5'
// https://mvnrepository.com/artifact/com.vladmihalcea/hibernate-types-55
hibernateTypesVersion = '2.19.2'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial
hibernateSpatialVersion = '5.6.11.Final'
hibernateSpatialVersion = '5.6.12.Final'
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
jtsVersion = '1.19.0'
// https://mvnrepository.com/artifact/com.github.liaochong/myexcel
myexcelVersion = '4.2.2'
// https://mvnrepository.com/artifact/org.jetbrains/annotations
jetbrainsAnnotationsVersion = '23.0.0'
}
allprojects {
......@@ -84,7 +86,7 @@ subprojects {
}
}
[compileJava,compileTestJava,javadoc]*.options*.encoding ='UTF-8'
[compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
......@@ -128,7 +130,7 @@ subprojects {
}
}
task hooks() {
task preCommit() {
// fix: CI/CD
try {
// GitHook pre-commit (spotless, spotbugs)
......@@ -141,12 +143,13 @@ task hooks() {
RESULT=\$?
exit \$RESULT
"""
} catch (ignored) {}
} catch (ignored) {
}
}
gradle.getTaskGraph().whenReady {
def skipHooks = gradle.startParameter.getSystemPropertiesArgs().containsKey('skip-hooks')
if (!skipHooks) {
hooks
preCommit
}
}
......@@ -3,6 +3,7 @@
> IDEA
<!-- prettier-ignore -->
-[IDE Eval Rest](https://www.cnblogs.com/wang-cong/p/15150585.html) - IDEA 无限重置试用插件
-[GitHub Copilot](https://plugins.jetbrains.com/plugin/17718-github-copilot)
-[.ignore](https://plugins.jetbrains.com/plugin/7495--ignore)
......@@ -10,6 +11,7 @@
-[Grep Console](https://plugins.jetbrains.com/plugin/7125-grep-console)
-[Rainbow Brackets](https://plugins.jetbrains.com/plugin/10080-rainbow-brackets)
-[Spotless Gradle](https://plugins.jetbrains.com/plugin/18321-spotless-gradle)
-[Easy I18n](https://plugins.jetbrains.com/plugin/16316-easy-i18n)
- [Prettier](https://plugins.jetbrains.com/plugin/10456-prettier)
- [Nyan Progress Bar](https://plugins.jetbrains.com/plugin/8575-nyan-progress-bar)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论