提交 8df0cf55 作者: 方治民

feat: 同步 3.x 版本相关配置实现及问题修复

上级 1b6d2cee
......@@ -46,12 +46,10 @@ dependencies {
implementation "io.minio:minio:${minioVersion}"
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
// Optional: 扩展实现在文件上传时对文件进行预处理,依赖 Minio 模块
// https://mvnrepository.com/artifact/org.bytedeco/javacv
implementation 'org.bytedeco:javacv:1.5.7'
// https://mvnrepository.com/artifact/org.bytedeco/ffmpeg-platform
implementation 'org.bytedeco:ffmpeg-platform:5.0-1.5.7'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
......
......@@ -5,13 +5,15 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@EntityScan(
basePackageClasses = { Application.class, Jsr310JpaConverters.class },
basePackages = Application.BASE_PACKAGES
)
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = Application.BASE_PACKAGES)
@SpringBootApplication(scanBasePackages = Application.BASE_PACKAGES)
public class Application {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.app.config;
import cn.dev33.satoken.exception.*;
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 异常信息
......
......@@ -12,6 +12,7 @@ import javax.persistence.Table;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
/**
......@@ -25,7 +26,7 @@ import org.hibernate.annotations.Comment;
@Getter
@Setter
@ToString
@Builder
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
......
......@@ -78,7 +78,7 @@ public class ExampleController {
String text = i18n.get("example.hello");
List<String> data = Arrays.asList(text.split(" "));
PageVo<String> vo = PageVo.build(data, data.size());
PageVo<String> vo = PageVo.build(data, data.size(), 1);
return Result.ok(vo);
}
......
/* (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(1)
@RestControllerAdvice
@RequiredArgsConstructor
public class AuthExceptionHandler {
/**
* 未登录异常(鉴权失败)
*
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(NotLoginException.class)
public Result<String> unauthorizedErrorHandler() {
return Result.no(Status.UNAUTHORIZED);
}
/**
* 1. 二级认证失败异常
* 2. 角色条件不满足
* 3. 权限条件不满足
* 4. HTTP Basic 验证不通过
* 5. 用户被禁止访问该服务
* 6. API 被禁用
*
* @return 异常信息反馈 {@link Status#FORBIDDEN
*/
@ExceptionHandler(
{
// https://sa-token.dev33.cn/doc.html#/up/safe-auth
NotSafeException.class,
// https://sa-token.dev33.cn/doc.html#/use/at-check
NotRoleException.class,
NotPermissionException.class,
// https://sa-token.dev33.cn/doc.html#/up/basic-auth
NotBasicAuthException.class,
// https://sa-token.dev33.cn/doc.html#/up/disable
DisableServiceException.class,
ApiDisabledException.class,
}
)
public Result<String> forbiddenHandler(Exception e) {
if (e instanceof NotSafeException) {
return Result.no(Status.FORBIDDEN, "Code.10000");
}
if (e instanceof NotRoleException) {
return Result.no(Status.FORBIDDEN, "Code.10001");
}
if (e instanceof NotPermissionException) {
return Result.no(Status.FORBIDDEN, "Code.10002");
}
if (e instanceof NotBasicAuthException) {
return Result.no(Status.FORBIDDEN, "Code.10003");
}
log.warn(e.getMessage(), e);
return Result.no(Status.FORBIDDEN);
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.auth.config;
import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import java.util.Optional;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
/**
* @author Jim
* @version 0.1
* 2023/3/3 11:43
*/
@Component
@Configurable
@RequiredArgsConstructor
public class InjectAuditorAware implements AuditorAware<String> {
final UserRepository userRepository;
@Override
public @NonNull Optional<String> getCurrentAuditor() {
if (StpUtil.isLogin()) {
String loginId = StpUtil.getLoginIdAsString();
Optional<User> optional = userRepository.findById(loginId);
if (optional.isPresent()) {
return Optional.of(optional.get().getId());
}
}
return Optional.empty();
}
}
......@@ -12,8 +12,9 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 获取登录用户权限信息实现
......@@ -23,11 +24,12 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37
*/
@Transactional(readOnly = true)
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
......@@ -50,14 +52,15 @@ public class StpInterfaceImpl implements StpInterface {
/**
* 根据 id 获取用户信息
*
* @param loginId 登录 ID
* @return 用户信息
*/
private User getUser(Object loginId) {
public User getUser(Object loginId) {
String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception("用户不存在");
throw Status.NOT_FOUND.exception("Code.1000");
}
return optional.get();
......
......@@ -5,6 +5,7 @@ import static com.yiring.auth.domain.permission.Permission.DELETE_SQL;
import static com.yiring.auth.domain.permission.Permission.TABLE_NAME;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.yiring.common.domain.BasicEntity;
import java.io.Serial;
import java.io.Serializable;
......@@ -15,7 +16,6 @@ import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
......@@ -27,30 +27,29 @@ import org.hibernate.annotations.Where;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@SQLDelete(sql = DELETE_SQL + BasicEntity.Where.WHERE_ID)
//@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = {
@Index(columnList = "deleteTime"),
@Index(columnList = "type"),
@Index(columnList = "pid"),
@Index(columnList = "tree"),
@Index(columnList = BasicEntity.Fields.deleted),
@Index(columnList = Permission.Fields.type),
@Index(columnList = Permission.Fields.pid),
@Index(columnList = Permission.Fields.tree),
}
)
@Comment("系统权限")
public class Permission extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_PERMISSION";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -2001221843529000953L;
......@@ -86,8 +85,9 @@ public class Permission extends BasicEntity implements Serializable {
@Comment("是否隐藏")
Boolean hidden;
@Comment("是否启用")
Boolean enable;
@Comment("是否禁用")
@Column(columnDefinition = "bool default false")
Boolean disabled;
@Comment("权限父级ID")
String pid;
......@@ -108,21 +108,38 @@ public class Permission extends BasicEntity implements Serializable {
/**
* 目录/平台
*/
DIR,
DIR("目录"),
/**
* 菜单
*/
MENU,
MENU("菜单"),
/**
* 按钮
*/
BUTTON,
BUTTON("按钮");
final String name;
Type(String name) {
this.name = name;
}
@JsonCreator
public static Type of(String type) {
for (Type value : values()) {
if (value.name().equalsIgnoreCase(type)) {
return value;
}
}
return null;
}
}
/**
* 获取权限的元数据信息,通常是根据前端所需来输出,可自定义调整
*
* @return JSON 格式 Meta 元数据
*/
public JSONObject getMetaJson() {
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.role;
import static com.yiring.auth.domain.role.Role.DELETE_SQL;
import static com.yiring.auth.domain.role.Role.TABLE_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore;
......@@ -19,7 +18,6 @@ import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
......@@ -31,25 +29,25 @@ import org.hibernate.annotations.Where;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@SQLDelete(sql = Permission.DELETE_SQL + BasicEntity.Where.WHERE_ID)
//@SQLDeleteAll(sql = Permission.DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = { @Index(columnList = "deleteTime"), @Index(columnList = "uid,deleteTime", unique = true) }
indexes = @Index(columnList = BasicEntity.Fields.deleted),
uniqueConstraints = { @UniqueConstraint(columnNames = { Role.Fields.uid, BasicEntity.Fields.deleted }) }
)
@Comment("系统角色")
public class Role extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_ROLE";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
@Serial
private static final long serialVersionUID = 910404402503275957L;
......@@ -62,8 +60,9 @@ public class Role extends BasicEntity implements Serializable {
@Column(nullable = false)
String name;
@Comment("是否启用")
Boolean enable;
@Comment("是否禁用")
@Column(columnDefinition = "bool default false")
Boolean disabled;
@JsonIgnore
@Builder.Default
......
......@@ -3,6 +3,7 @@ package com.yiring.auth.domain.role;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
......@@ -13,4 +14,4 @@ import org.springframework.stereotype.Repository;
*/
@Repository
public interface RoleRepository extends JpaRepository<Role, Serializable> {}
public interface RoleRepository extends JpaRepository<Role, Serializable>, JpaSpecificationExecutor<Role> {}
......@@ -2,6 +2,7 @@
package com.yiring.auth.domain.user;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.common.domain.BasicEntity;
import java.io.Serial;
......@@ -16,8 +17,6 @@ import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
* 用户
......@@ -28,31 +27,28 @@ import org.hibernate.annotations.Where;
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = User.DELETE_SQL)
@SQLDeleteAll(sql = User.DELETE_SQL)
@Where(clause = User.Where.EXIST)
@SQLDelete(sql = Permission.DELETE_SQL + BasicEntity.Where.WHERE_ID)
//@SQLDeleteAll(sql = Permission.DELETE_SQL)
@Entity
@Table(
name = User.TABLE_NAME,
indexes = {
@Index(columnList = "enabled"),
@Index(columnList = "deleteTime"),
@Index(columnList = "username,deleteTime", unique = true),
@Index(columnList = "mobile,deleteTime", unique = true),
@Index(columnList = "email,deleteTime", unique = true),
indexes = { @Index(columnList = User.Fields.disabled), @Index(columnList = BasicEntity.Fields.deleted) },
uniqueConstraints = {
@UniqueConstraint(columnNames = { User.Fields.username, BasicEntity.Fields.deleted }),
@UniqueConstraint(columnNames = { User.Fields.mobile, BasicEntity.Fields.deleted }),
@UniqueConstraint(columnNames = { User.Fields.email, BasicEntity.Fields.deleted }),
}
)
@Comment("系统用户")
public class User extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_USER";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -5787847701210907511L;
......@@ -75,8 +71,8 @@ public class User extends BasicEntity implements Serializable {
@Comment("密码")
String password;
@Comment("是否用")
Boolean enabled;
@Comment("是否用")
Boolean disabled;
@JsonIgnore
@Builder.Default
......
......@@ -3,6 +3,7 @@ package com.yiring.auth.domain.user;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
......@@ -14,31 +15,7 @@ import org.springframework.stereotype.Repository;
*/
@Repository
public interface UserRepository extends JpaRepository<User, Serializable> {
/**
* 根据用户名称查询用户信息
*
* @param username 用户名
* @return 用户信息
*/
User findByUsername(String username);
/**
* 根据手机号查询用户信息
*
* @param mobile 手机号
* @return 用户信息
*/
User findByMobile(String mobile);
/**
* 根据邮箱查询用户信息
*
* @param email 邮箱
* @return 用户信息
*/
User findByEmail(String email);
public interface UserRepository extends JpaRepository<User, Serializable>, JpaSpecificationExecutor<User> {
/**
* 根据用户名/手机号/邮箱查询用户信息
*
......
......@@ -51,9 +51,6 @@ public class RegisterParam implements Serializable {
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "简介", example = "平台管理员")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
Boolean enable;
@ApiModelProperty(value = "是否禁用", example = "false")
Boolean disabled;
}
......@@ -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,15 @@ import org.springframework.stereotype.Component;
@SuppressWarnings("unused")
@Component
@RequiredArgsConstructor
public class Auths {
@Resource
UserRepository userRepository;
final UserRepository userRepository;
/**
* 管理员用户
*/
public static final String ADMIN_USER = "admin";
/**
* 管理员角色标识
......@@ -36,6 +41,7 @@ public class Auths {
/**
* 根据 Token 获取用户信息
* 如果用户未登录或校验失败会抛出 NotLoginException {@link Status#UNAUTHORIZED}
*
* @param token token
* @return 用户信息
*/
......@@ -47,7 +53,11 @@ public class Auths {
}
Optional<User> optional = userRepository.findById(Objects.toString(id));
if (optional.isEmpty()) {
if (
optional.isEmpty() ||
Boolean.TRUE.equals(optional.get().getDeleted()) ||
Boolean.TRUE.equals(optional.get().getDisabled())
) {
StpUtil.logout(id);
throw NotLoginException.newInstance(StpUtil.TYPE, NotLoginException.INVALID_TOKEN);
}
......@@ -70,6 +80,7 @@ public class Auths {
/**
* 踢出这个用户 id 所有登录状态(可能有多人重复登录了一个账号的情况)
*
* @param userId 用户 id
*/
public void logoutAll(String userId) {
......@@ -81,6 +92,7 @@ public class Auths {
/**
* 判断用户是否为超级管理员
*
* @param userId 用户 ID
* @return 是否为管理员
*/
......@@ -91,19 +103,24 @@ public class Auths {
/**
* 检查用户是否为管理员(检查用户是否拥有包含 admin 字符的角色)
*
* @param user 用户
* @return 是否为管理员
*/
public boolean isAdmin(User user) {
return user
.getRoles()
.stream()
.anyMatch(role -> Boolean.TRUE.equals(role.getEnable()) && ADMIN_ROLES.contains(role.getUid()));
return (
ADMIN_USER.equals(user.getUsername()) ||
user
.getRoles()
.stream()
.anyMatch(role -> Boolean.FALSE.equals(role.getDisabled()) && ADMIN_ROLES.contains(role.getUid()))
);
}
/**
* 检查当前登录用户是否为管理员
* {@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;
......@@ -14,6 +13,7 @@ import java.util.stream.Collectors;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import org.springframework.beans.BeanUtils;
import org.springframework.lang.Nullable;
/**
* 权限工具栏
......@@ -28,6 +28,7 @@ public class Permissions {
/**
* 将角色集合转换成 Vo 集合
*
* @param roles 角色集合
* @return vos
*/
......@@ -40,6 +41,7 @@ public class Permissions {
/**
* 将权限集合转换成菜单树
*
* @param permissions 权限集合
* @return 菜单树
*/
......@@ -75,10 +77,11 @@ public class Permissions {
/**
* 菜单树递归排序
*
* @param menus 菜单集合
* @return 排序后的菜单集合
*/
public static List<MenuVo> sortMenuTreeVo(@Nullable List<MenuVo> menus) {
public static List<MenuVo> sortMenuTreeVo(List<MenuVo> menus) {
return menus
.stream()
.sorted(
......@@ -88,7 +91,7 @@ public class Permissions {
)
)
.peek(item -> {
if (!Commons.isNullOrEmpty(item.getChildren())) {
if (Commons.isNotEmpty(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 权限集合
*/
......@@ -123,6 +128,7 @@ public class Permissions {
.stream()
.map(Role::getPermissions)
.flatMap(Set::stream)
.filter(permission -> Boolean.FALSE.equals(permission.getDeleted()))
.distinct()
.sorted(Comparator.comparing(Permission::getTree, Comparator.comparingInt(String::length)))
.collect(Collectors.toList());
......@@ -130,8 +136,9 @@ public class Permissions {
/**
* 根据 pid 构建树状权限集合
*
* @param permissions 权限集合
* @param pid 权限父级 ID
* @param pid 权限父级 ID
* @return 树状权限集合
*/
public List<PermissionVo> toTree(List<Permission> permissions, @NonNull String pid) {
......
......@@ -81,7 +81,7 @@ public class AuthController {
.realName(param.getRealName())
.username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable())
.disabled(param.getDisabled())
.createTime(LocalDateTime.now())
.build();
userRepository.saveAndFlush(user);
......@@ -97,22 +97,22 @@ public class AuthController {
throw BusinessException.i18n("Code.100003");
}
// 检查密码
String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) {
throw BusinessException.i18n("Code.100003");
}
// 检查用户是否已被删除
if (user.getDeleteTime() != null) {
if (Boolean.TRUE.equals(user.getDeleted())) {
throw BusinessException.i18n("Code.100004");
}
// 检查用户是否被允许登录
if (!Boolean.TRUE.equals(user.getEnabled())) {
if (Boolean.TRUE.equals(user.getDisabled())) {
throw BusinessException.i18n("Code.100005");
}
// 检查密码
String cps = SaSecureUtil.sha256(param.getPassword());
if (!cps.equals(user.getPassword())) {
throw BusinessException.i18n("Code.100003");
}
// 更新用户信息
user.setLastLoginIp(Commons.getClientIpAddress(request));
user.setLastLoginTime(LocalDateTime.now());
......
......@@ -119,7 +119,7 @@ public class PermissionController {
public Result<PageVo<PermissionVo>> page(@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());
PageVo<PermissionVo> vo = PageVo.build(data, page.getTotalElements(), page.getTotalPages());
return Result.ok(vo);
}
......
......@@ -133,7 +133,7 @@ public class RoleController {
public Result<PageVo<RoleVo>> page(@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());
PageVo<RoleVo> vo = PageVo.build(data, page.getTotalElements(), page.getTotalPages());
return Result.ok(vo);
}
......
......@@ -73,7 +73,7 @@ public class UserController {
Page<User> page = userRepository.findAll(PageParam.toPageable(param));
List<UserVo> data = page.get().map(user -> Commons.transform(user, UserVo.class)).collect(Collectors.toList());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements(), page.getTotalPages());
return Result.ok(vo);
}
}
......@@ -3,6 +3,7 @@ package com.yiring.auth.web.user;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.util.Auths;
import com.yiring.auth.util.Permissions;
......@@ -40,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
public class UserViewController {
final Auths auths;
final PermissionRepository permissionRepository;
@ApiOperation(value = "获取登录用户信息")
@GetMapping("getUserInfo")
......@@ -61,8 +63,16 @@ public class UserViewController {
@GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() {
User user = auths.getLoginUser();
List<Permission> permissions = Permissions
.toPermissions(user.getRoles())
// FIXED: admin 用户默认可以查询到所有菜单
List<Permission> list;
if (Auths.ADMIN_USER.equalsIgnoreCase(user.getUsername())) {
list = permissionRepository.findAll();
} else {
list = Permissions.toPermissions(user.getRoles());
}
List<Permission> permissions = list
.stream()
.filter(permission -> !Permission.Type.BUTTON.equals(permission.getType()))
.collect(Collectors.toList());
......
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
......
......@@ -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", "/v2/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();
}
}
// 计算接口执行耗时
......
/* (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 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.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() {}
}
......@@ -3,6 +3,8 @@ package com.yiring.common.config;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
......@@ -37,30 +39,7 @@ public class EnvConfig implements Serializable {
boolean prod;
/**
* 扩展配置
* 其他配置
*/
Extra extra;
/**
* 扩展环境配置变量
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.extra")
@ConfigurationProperties(prefix = "env.extra")
public static class Extra implements Serializable {
@Serial
private static final long serialVersionUID = -521508901960998020L;
/**
* 公共用户名
*/
String username;
/**
* 公共密码
*/
String password;
}
Map<String, String> props = new HashMap<>();
}
......@@ -2,9 +2,10 @@
package com.yiring.common.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
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,21 +19,25 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@RequiredArgsConstructor
public class JacksonConfig {
@Resource
JavaTimeModule javaTimeModule;
final JavaTimeModule javaTimeModule;
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// 忽略空值
// 忽略空值(序列化)
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 忽略未知字段(反序列化)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Java 8 Date/Time support
mapper.registerModule(javaTimeModule);
// JTS Geometry support
mapper.registerModule(new JtsModule());
// feat: add AdminServerModule
// TODO: add AdminServerModule
return mapper;
}
}
/* (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 java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
......@@ -29,7 +31,7 @@ import org.jetbrains.annotations.PropertyKey;
@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;
......@@ -97,7 +99,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 +110,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 +125,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 +140,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 +150,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 +160,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 +170,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 +180,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 +190,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) && Objects.nonNull(details)) {
String prefix = "Code.";
if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, "");
code = Convert.toInt(codeText);
} else {
code = -1;
}
}
Result<T> result = (Result<T>) Result
.builder()
.status(status.value())
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.domain;
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import com.vladmihalcea.hibernate.type.json.JsonType;
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.annotations.Comment;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.snowflake.SnowflakeId;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* 基础表抽象类
......@@ -25,16 +29,12 @@ import org.hibernate.snowflake.SnowflakeId;
@ToString
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder(toBuilder = true)
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SuperBuilder(toBuilder = true)
@TypeDefs(
value = {
@TypeDef(name = "json", typeClass = JsonType.class), @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class),
}
)
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BasicEntity {
public class BasicEntity {
@Comment("主键")
@Id
......@@ -42,22 +42,32 @@ public abstract class BasicEntity {
@GenericGenerator(name = SnowflakeId.GENERATOR, strategy = SnowflakeId.Strategy.STRING)
String id;
@Comment("创建人")
@CreatedBy
String createBy;
@Comment("创建时间")
@Column(nullable = false)
@CreationTimestamp
@CreatedDate
LocalDateTime createTime;
@Comment("修改人")
@LastModifiedBy
String updateBy;
@Comment("最后修改时间")
@Column(nullable = false)
@UpdateTimestamp
@LastModifiedDate
LocalDateTime updateTime;
@Builder.Default
@Comment("删除时间")
LocalDateTime deleteTime;
@Column(columnDefinition = "bool default false")
Boolean deleted = Boolean.FALSE;
public interface Where {
String EXIST = " delete_time is null ";
String EXIST = " deleted = false ";
String DELETE_SET = " set deleted = true ";
String DELETE_SET = " set delete_time = now() where id = ? ";
String WHERE_ID = " where id = ? ";
}
}
/* (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) 2023 YiRing, Inc. */
package com.yiring.common.utils;
import com.yiring.common.domain.BasicEntity;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.springframework.data.domain.Example;
import org.springframework.data.jpa.domain.Specification;
/**
* @author Jim
* @version 0.1
* 2023/3/17 16:36
*/
@SuppressWarnings("unused")
@UtilityClass
public class Specifications {
/**
* 构建谓词条件,支持扩展条件
*
* @param fn 回调函数
* @param <T> Entity 类型
* @return Specification
*/
public static <T> Specification<T> of(
CallbackFunction<Root<T>, CriteriaQuery<?>, CriteriaBuilder, List<Predicate>> fn
) {
return build(fn, null);
}
/**
* 构建存在的谓词条件,支持扩展条件
*
* @param fn 回调函数
* @param <T> Entity 类型
* @return Specification
*/
public static <T> Specification<T> exist(
CallbackFunction<Root<T>, CriteriaQuery<?>, CriteriaBuilder, List<Predicate>> fn
) {
return build(fn, false);
}
/**
* 构建存在的谓词条件
*
* @param <T> Entity 类型
* @return Specification
*/
public static <T> Specification<T> exist() {
return build(null, false);
}
/**
* 构建存在的 Example
*
* @param type Entity 类型
* @return Example
*/
@SneakyThrows
public static <T extends BasicEntity> Example<T> exist(Class<T> type) {
Constructor<T> declaredConstructor = type.getDeclaredConstructor();
T probe = declaredConstructor.newInstance();
probe.setDeleted(false);
return Example.of(probe);
}
/**
* 构建存在的谓词条件
*
* @param fn 回调函数
* @param deleted 是否删除
* @param <T> Entity 类型
* @return Specification
*/
private static <T> Specification<T> build(
CallbackFunction<Root<T>, CriteriaQuery<?>, CriteriaBuilder, List<Predicate>> fn,
Boolean deleted
) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (Objects.nonNull(deleted)) {
predicates.add(cb.equal(root.get(BasicEntity.Fields.deleted), deleted));
}
if (Objects.nonNull(fn)) {
fn.apply(root, query, cb, predicates);
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
}
/**
* 构建存在的谓词条件
*
* @param root 根
* @param cb 构建器
* @return 谓词
*/
public static List<Predicate> buildExistPredicates(Root<?> root, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get(BasicEntity.Fields.deleted), false));
return predicates;
}
@FunctionalInterface
public interface CallbackFunction<R, Q, C, P> {
void apply(R root, Q query, C cb, P predicates);
}
}
......@@ -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.isNotEmpty(constraintViolations)) {
throw new ConstraintViolationException(constraintViolations);
}
}
......
......@@ -5,37 +5,41 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 键值对输出
* 选项 VO
*
* @author ifzm
* @version 0.1
* 2022/3/24 17:29
*/
@ApiModel(value = "KeyValueVo", description = "键值对响应输出")
@ApiModel(value = "OptionVo", description = "选项 VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class KeyValueVo implements Serializable {
public class OptionVo implements Serializable {
@Serial
private static final long serialVersionUID = -5238793972067296346L;
@ApiModelProperty(value = "key", example = "key")
String key;
private static final long serialVersionUID = 7178232019485233157L;
@ApiModelProperty(value = "value", example = "value")
String value;
/**
* 扩展字段,可用于文本输出
*/
@ApiModelProperty(value = "label", example = "label")
String label;
@ApiModelProperty(value = "disabled", example = "false")
Boolean disabled;
@ApiModelProperty(value = "extra")
Object extra;
@ApiModelProperty(value = "children", example = "[]")
List<OptionVo> children;
}
......@@ -9,7 +9,10 @@ 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;
......@@ -35,6 +38,9 @@ public class PageVo<T extends Serializable> implements Serializable {
@ApiModelProperty(value = "数据总数", example = "100", required = true)
Long total;
@ApiModelProperty(value = "分页页数", example = "10", required = true)
Integer pages;
/**
* 通常在带有时效性的数据查询时有用途(可选参数)
*/
......@@ -49,7 +55,7 @@ public class PageVo<T extends Serializable> implements Serializable {
*/
@SuppressWarnings({ "unused" })
public static <R extends Serializable> PageVo<R> build(List<R> data) {
return build(data, data.size());
return build(data, data.size(), 1);
}
/**
......@@ -60,8 +66,8 @@ public class PageVo<T extends Serializable> implements Serializable {
* @return PageVo
*/
@SuppressWarnings({ "unused" })
public static <R extends Serializable> PageVo<R> build(List<R> data, long total) {
return build(data, total, null);
public static <R extends Serializable> PageVo<R> build(List<R> data, long total, int pages) {
return build(data, total, pages, null);
}
/**
......@@ -73,10 +79,11 @@ public class PageVo<T extends Serializable> implements Serializable {
* @return PageVo
*/
@SuppressWarnings({ "unused" })
public static <R extends Serializable> PageVo<R> build(List<R> data, long total, LocalDateTime latest) {
public static <R extends Serializable> PageVo<R> build(List<R> data, long total, int pages, LocalDateTime latest) {
PageVo<R> vo = new PageVo<>();
vo.setData(data);
vo.setTotal(total);
vo.setPages(pages);
vo.setLatest(latest);
return vo;
}
......@@ -93,7 +100,7 @@ public class PageVo<T extends Serializable> implements Serializable {
@SuppressWarnings({ "unused" })
public static <S, T extends Serializable> PageVo<T> toPageVo(Page<S> page, Class<T> type) {
List<T> data = Commons.transform(page.toList(), type);
return build(data, page.getTotalElements());
return build(data, page.getTotalElements(), 1);
}
/**
......@@ -101,6 +108,6 @@ public class PageVo<T extends Serializable> implements Serializable {
*/
@SuppressWarnings("unused")
public static <T extends Serializable> PageVo<T> empty() {
return PageVo.build(Collections.emptyList(), 0);
return build(Collections.emptyList(), 0, 1);
}
}
......@@ -3,9 +3,11 @@ package com.yiring.common.config;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
......@@ -18,15 +20,34 @@ import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
* 2022/8/17 10:33
*/
@Order
@Configuration
@RequiredArgsConstructor
public class I18nConfig {
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* 解决多模块 i18n/messages 重复文件的读取问题
* <a href="https://stackoverflow.com/questions/17661252/spring-multi-module-i18n-with-modules-extending-the-messagesource-contents?noredirect=1&lq=1">...</a>
*
* @return MessageSource
*/
@Bean
public MessageSource messageSource() {
SmReloadableResourceBundleMessageSource messageSource = new SmReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath*:i18n/messages", "classpath:i18n/status", "classpath:i18n/validation");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setDefaultLocale(DEFAULT_LOCALE);
return messageSource;
}
@Bean
public LocaleResolver localeResolver() {
// header accept-language
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setDefaultLocale(DEFAULT_LOCALE);
return resolver;
}
......@@ -34,9 +55,8 @@ public class I18nConfig {
public LocalValidatorFactoryBean getValidator() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
messageSource.setDefaultLocale(DEFAULT_LOCALE);
messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation");
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
return bean;
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.config;
import java.io.IOException;
import java.util.Properties;
import lombok.NonNull;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* @author Jim
* @version 0.1
* 2023/1/20 18:00
*/
public class SmReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected @NonNull PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
if (filename.startsWith(PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) {
return refreshClassPathProperties(filename, propHolder);
} else {
return super.refreshProperties(filename, propHolder);
}
}
private PropertiesHolder refreshClassPathProperties(String filename, PropertiesHolder propHolder) {
Properties properties = new Properties();
long lastModified = -1;
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder);
properties.putAll(holder.getProperties());
if (lastModified < resource.lastModified()) lastModified = resource.lastModified();
}
} catch (IOException ignored) {}
return new PropertiesHolder(properties, lastModified);
}
}
......@@ -7,6 +7,7 @@ import org.jetbrains.annotations.PropertyKey;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
......@@ -18,6 +19,7 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Order
@Component
@RequiredArgsConstructor
public class I18n {
......
......@@ -12,7 +12,10 @@ import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import lombok.Cleanup;
import lombok.NonNull;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
......@@ -87,7 +90,7 @@ public record Minio(MinioConfig config, MinioClient client) {
.bucket(config.getBucket())
.stream(is, -1, PutObjectArgs.MIN_MULTIPART_SIZE)
.object(object)
.contentType(contentType)
.contentType(Optional.ofNullable(contentType).orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE))
.build();
return client.putObject(args);
}
......@@ -106,7 +109,9 @@ public record Minio(MinioConfig config, MinioClient client) {
}
Path path = file.toPath();
return putObject(Files.newInputStream(path), Files.probeContentType(path), object);
@Cleanup
InputStream inputStream = Files.newInputStream(path);
return putObject(inputStream, Files.probeContentType(path), object);
}
/**
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务
*
* @author Jim
* @version 0.1
* 2023/1/29 16:18
*/
public interface FileManageService {
/**
* 文件上传
*
* @param file 文件
* @return 文件地址
*/
String upload(MultipartFile file) throws Exception;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.service;
import java.io.InputStream;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传媒体文件预处理服务
......@@ -17,12 +17,12 @@ public interface UploadProcessService {
* PDF:haha.pdf -> haha.P12.pdf 记录 PDF 文件总页数,P12 即代表 PDF 总共 12 页,同时可通过 haha.P12.pdf.1.jpg... 按序号读取到每一页 PDF 对应的图片
* 音视频:haha.mp3/4 -> haha.T12.mp3/4 记录音视频的时长(秒),T12 即代表音视频时长为 12 秒
* 视频封面:haha.mp4 -> haha.mp4.jpg 截取视频封面
*
* @param object 上传文件存储地址
* @param is 文件流
* @param file 上传的文件
* @return 预处理后的文件地址(可能对文件名追加了时长、页数、分辨率等标识)
*/
@SuppressWarnings("unused")
default String handle(String object, InputStream is) {
default String handle(String object, MultipartFile file) {
return object;
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import com.yiring.common.service.UploadProcessService;
import org.springframework.stereotype.Service;
/**
* @author Jim
* @version 0.1
* 2023/2/13 11:14
*/
@Service
public class DefaultUploadProcessServiceImpl implements UploadProcessService {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.core.Minio;
import com.yiring.common.service.FileManageService;
import com.yiring.common.service.UploadProcessService;
import java.io.InputStream;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务实现
*
* @author Jim
* @version 0.1
* 2023/1/29 16:22
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FileManageServiceImpl implements FileManageService {
final Minio minio;
final UploadProcessService service;
@Override
public String upload(MultipartFile file) throws Exception {
// 获取文件名信息
String filename = file.getOriginalFilename();
if (StrUtil.isBlank(filename)) {
throw new RuntimeException("上传文件名不能为空");
}
// 获取文件信息以及默认存储地址
String uuid = IdUtil.fastSimpleUUID();
String object = minio.buildUploadPath(filename, "", uuid);
// 预处理(默认不做任何处理,具体逻辑需自行在外部实现)
object = service.handle(object, file);
// 上传原文件(如果是转换成了 m3u8 hls 文件则不保存原文件)
String suffix = ".m3u8";
if (filename.endsWith(suffix) || !object.endsWith(suffix)) {
@Cleanup
InputStream stream = file.getInputStream();
minio.putObject(stream, file.getContentType(), object);
}
return minio.getDefaultURI(object);
}
}
......@@ -8,7 +8,7 @@ import com.yiring.common.core.Minio;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.param.DownloadParam;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.service.FileManageService;
import com.yiring.common.util.FileUtils;
import com.yiring.common.vo.ImageInfo;
import io.minio.GetObjectResponse;
......@@ -45,7 +45,7 @@ import org.springframework.web.multipart.MultipartFile;
public class MinioController {
final Minio minio;
final UploadProcessService service;
final FileManageService fileManageService;
/**
* minio 上传文件,成功返回文件 url
......@@ -54,22 +54,8 @@ public class MinioController {
@PostMapping(value = "upload", headers = HttpHeaders.CONTENT_TYPE + "=" + MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@ApiParam(value = "文件", required = true) @RequestPart("file") MultipartFile file) {
try {
// 获取文件名信息
String filename = file.getOriginalFilename();
if (filename == null) {
throw Status.BAD_REQUEST.exception();
}
// 获取文件信息以及默认存储地址
String uuid = IdUtil.fastSimpleUUID();
String object = minio.buildUploadPath(filename, "", uuid);
// 预处理(默认不做任何处理,具体逻辑需自行在外部实现)
object = service.handle(object, file.getInputStream());
// 上传原文件
minio.putObject(file.getInputStream(), file.getContentType(), object);
return Result.ok(minio.getDefaultURI(object));
String link = fileManageService.upload(file);
return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
......@@ -86,10 +72,8 @@ public class MinioController {
)
@PostMapping(value = "uploadBase64Image")
public Result<String> uploadBase64Image(@NotBlank(message = "图片 Base64 信息不能为空") String base64Image) {
try {
// 解析 Base64 图片信息
ImageInfo image = FileUtils.parseBase64ImageText(base64Image);
// 解析 Base64 图片信息
try (ImageInfo image = FileUtils.parseBase64ImageText(base64Image)) {
// 获取文件信息以及默认存储地址
String uuid = IdUtil.getSnowflakeNextIdStr();
String object = minio.buildUploadPath(
......
......@@ -20,4 +20,9 @@ public @interface Times {
* 描述
*/
String value() default "";
/**
* 阈值(毫秒),当计算耗时超过阈值时,打印警告日志
*/
long threshold() default 0;
}
......@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
......@@ -30,12 +31,22 @@ public class TimesAspect {
@Around("times()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
try {
return point.proceed();
} finally {
long end = System.currentTimeMillis();
log.info("[Times] {}: {} ms", getTimesValue(point), end - start);
Object result = point.proceed();
long end = System.currentTimeMillis();
long times = end - start;
// 计算是否超过阈值,打印日志
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Times annotation = method.getAnnotation(Times.class);
String name = StrUtil.isEmpty(annotation.value()) ? method.getName() : annotation.value();
if (annotation.threshold() > 0 && times > annotation.threshold()) {
log.warn("[Times] {}: {} ms", name, times);
} else {
log.info("[Times] {}: {} ms", name, times);
}
return result;
}
public String getTimesValue(JoinPoint joinPoint) {
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.util;
import cn.hutool.core.collection.CollUtil;
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;
import org.springframework.beans.BeanUtils;
......@@ -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 isNotEmpty(Collection<?> collection) {
return collection != null && !collection.isEmpty();
}
/**
......@@ -85,14 +87,19 @@ 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 {
if (Objects.isNull(source)) {
return null;
}
Constructor<T> declaredConstructor = type.getDeclaredConstructor();
// 实例化
T target = declaredConstructor.newInstance();
......@@ -106,33 +113,39 @@ 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) {
public <T, S> List<T> transform(List<S> list, Class<T> type, String... ignoreProperties) {
return transform(list, type, null, ignoreProperties);
}
/**
* 将集合通过 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(
@NonNull List<S> list,
List<S> list,
Class<T> type,
CallbackFunction<S, T> fn,
String... ignoreProperties
) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
try {
Constructor<T> declaredConstructor = type.getDeclaredConstructor();
......@@ -162,6 +175,7 @@ public class Commons {
public interface CallbackFunction<S, T> {
/**
* 执行方法
*
* @param s 源对象
* @param t 目标对象
*/
......
......@@ -3,13 +3,9 @@ package com.yiring.common.util;
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 java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
......@@ -18,8 +14,8 @@ import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.util.FileCopyUtils;
/**
* 文件工具类
......@@ -37,9 +33,9 @@ public class FileUtils {
*
* @param response HttpServletResponse
* @param file File
* @throws IOException IOException
*/
public void download(HttpServletResponse response, File file) throws IOException {
@SneakyThrows
public void download(HttpServletResponse response, File file) {
String filename = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8);
BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
long lastModified = basicFileAttributes.lastModifiedTime().toMillis();
......@@ -47,7 +43,13 @@ public class FileUtils {
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(basicFileAttributes.size()));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(FileUtil.getMimeType(file.toPath()));
FileReader.create(file).writeToStream(response.getOutputStream(), true);
try (
FileInputStream object = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
}
/**
......@@ -59,8 +61,8 @@ public class FileUtils {
* @param filename 文件名称(带后缀)
* @param contentType 文档类型
* @param lastModified 最后修改时间
* @throws IOException IOException
*/
@SneakyThrows
public void download(
HttpServletResponse response,
InputStream object,
......@@ -68,15 +70,19 @@ public class FileUtils {
String filename,
String contentType,
long lastModified
) throws IOException {
) {
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(contentType);
IOUtils.copy(object, response.getOutputStream());
object.close();
response.flushBuffer();
try (
object;
BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
}
/**
......
......@@ -22,7 +22,7 @@ import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ImageInfo implements Serializable {
public class ImageInfo implements Serializable, AutoCloseable {
@Serial
private static final long serialVersionUID = 4042804283860857802L;
......@@ -61,4 +61,11 @@ public class ImageInfo implements Serializable {
* 图片高度
*/
int height;
@Override
public void close() throws Exception {
if (stream != null) {
stream.close();
}
}
}
plugins {
id 'java'
// https://start.spring.io
id 'org.springframework.boot' version '2.7.11'
id 'org.springframework.boot' version '2.7.12'
// https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.1.0'
// https://plugins.gradle.org/plugin/com.diffplug.spotless
......@@ -15,31 +15,31 @@ ext {
// Spotless
// https://www.npmjs.com/package/prettier
prettierVersion = '2.8.7'
prettierVersion = '2.8.8'
// https://www.npmjs.com/package/prettier-plugin-java
prettierJavaVersion = '2.1.0'
// SpringCloud
// https://start.spring.io/
springCloudVersion = '2021.0.6'
springCloudVersion = '2021.0.7'
// Dependencies
// https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-spring-boot-starter
knife4jVersion = '2.0.9'
// https://mvnrepository.com/artifact/io.swagger/swagger-annotations
swaggerAnnotationsVersion = '1.6.10'
swaggerAnnotationsVersion = '1.6.11'
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot-starter
saTokenVersion = '1.34.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.18'
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
fastJsonVersion = '2.0.28'
fastJsonVersion = '2.0.32'
// https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core
xxlJobVersion = '2.4.0'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
okhttpVersion = '4.10.0'
okhttpVersion = '4.11.0'
// https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.5.2'
minioVersion = '8.5.3'
// https://mvnrepository.com/artifact/com.vladmihalcea/hibernate-types-55
hibernateTypesVersion = '2.21.1'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial
......@@ -52,6 +52,9 @@ ext {
jetbrainsAnnotationsVersion = '24.0.1'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
pdfboxVersion = '2.0.28'
// https://central.sonatype.com/artifact/net.bramp.ffmpeg/ffmpeg
// FIXED: ffmpeg 4.x
ffmpegWrapperVersion = '0.7.0'
}
allprojects {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论