提交 a62b1f28 作者: 方治民

feat: 分离用户菜单权限树查询接口、快速失败状态异常类型及全局处理、新增通用类型集合转换工具方法、hutool-json 换成 fastjson 依赖等细节优化处理

上级 353782a9
......@@ -30,4 +30,7 @@ dependencies {
implementation project(":basic-common:minio")
// FIX: minio dep
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
}
......@@ -6,6 +6,7 @@ import com.yiring.app.constant.Code;
import com.yiring.app.exception.CodeException;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.FailStatusException;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
......@@ -33,7 +34,7 @@ public class GlobalExceptionHandler {
* 参数校验异常
*
* @param e 异常信息
* @return 统一的校验失败信息 {@link Status#BAD_REQUEST
* @return 统一的校验失败信息 {@link Status#EXPECTATION_FAILED
*/
@ExceptionHandler(
value = { BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class }
......@@ -57,7 +58,7 @@ public class GlobalExceptionHandler {
}
}
return Result.no(Status.BAD_REQUEST, error);
return Result.no(Status.EXPECTATION_FAILED, error);
}
/**
......@@ -74,7 +75,6 @@ public class GlobalExceptionHandler {
/**
* 未登录异常(鉴权失败)
*
* @param e 异常信息
* @return 异常信息反馈 {@link Status#UNAUTHORIZED
*/
@ExceptionHandler(value = NotLoginException.class)
......@@ -83,12 +83,6 @@ public class GlobalExceptionHandler {
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler(value = ClientAbortException.class)
public void clientAbortExceptionHandler() {}
/**
* 自定义业务异常
*/
@ExceptionHandler(value = CodeException.class)
......@@ -98,6 +92,20 @@ public class GlobalExceptionHandler {
}
/**
* 失败状态异常
*/
@ExceptionHandler(value = FailStatusException.class)
public Result<String> customCodeExceptionHandler(FailStatusException e) {
return Result.no(e.getStatus());
}
/**
* 取消请求异常(忽略)
*/
@ExceptionHandler(value = ClientAbortException.class)
public void clientAbortExceptionHandler() {}
/**
* 其他异常
*
* @param e 异常信息
......
spring:
datasource:
url: jdbc:h2:mem:mockdb;DB_CLOSE_ON_EXIT=FALSE
url: jdbc:h2:file:~/h2_basic;DB_CLOSE_ON_EXIT=FALSE
username: sa
password: 123456
jpa:
......@@ -35,3 +35,8 @@ minio:
end-point: "http://127.0.0.1:18100"
bucket: basic
domain: ${minio.endpoint}/${minio.bucket}
logging:
level:
# sql bind parameter
org.hibernate.type.descriptor.sql.BasicBinder: trace
server:
port: 8181
servlet:
context-path: /basic-api
context-path: /api
spring:
application:
name: "basic-api-app"
profiles:
include: auth
active: dev
active: mock
# DEBUG
debug: false
......@@ -17,4 +17,7 @@ dependencies {
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.domain.permission;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.io.Serializable;
import javax.persistence.*;
import lombok.*;
......@@ -79,9 +81,22 @@ public class Permission implements Serializable {
@Comment("树节点标识")
String tree;
/**
* 可用于扩展一些前端可能用到的路由参数
*/
@Comment("扩展元数据信息")
@Lob
@Column(columnDefinition = "JSON")
String meta;
@SuppressWarnings({ "unused" })
public enum Type {
/**
* 目录/平台
*/
DIR,
/**
* 菜单
*/
MENU,
......@@ -91,4 +106,19 @@ public class Permission implements Serializable {
*/
BUTTON,
}
public JSONObject getMetaJson() {
JSONObject meta = new JSONObject();
meta.put("title", this.name);
meta.put("icon", this.icon);
meta.put("orderNo", this.serial);
meta.put("hideMenu", this.hidden);
try {
String raw = this.meta.replace("\\", "").replaceAll("^\"(.*)\"$", "$1");
meta.putAll(JSON.parseObject(raw));
} catch (Exception ignored) {}
return meta;
}
}
......@@ -68,8 +68,8 @@ public class User implements Serializable {
@Comment("密码")
String password;
@Comment("职称")
String title;
@Comment("简介")
String introduction;
@Comment("头像")
String avatar;
......
......@@ -43,14 +43,14 @@ public class RegisterParam implements Serializable {
@Pattern(regexp = "^1[0-9]{10}$", message = "手机号码格式不正确")
String mobile;
@ApiModelProperty(value = "头像", example = "http://img.ifzm.cn/cat.jpg")
@ApiModelProperty(value = "头像", example = "https://files.catbox.moe/96nu4q.jpg")
String avatar;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "职称", example = "平台管理员")
String title;
@ApiModelProperty(value = "简介", example = "平台管理员")
String introduction;
@ApiModelProperty(value = "是否启用", example = "true")
Boolean enable;
......
......@@ -62,4 +62,7 @@ public class PermissionParam implements Serializable {
@ApiModelProperty(value = "父级ID", example = "0")
@Builder.Default
Long pid = 0L;
@ApiModelProperty(value = "元数据", example = "{}")
String meta;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.util;
import com.sun.istack.Nullable;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo;
import com.yiring.common.util.Commons;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
......@@ -39,6 +43,41 @@ public class Permissions {
}
/**
* 将权限集合转换成菜单树
* @param permissions 权限集合
* @return 菜单树
*/
public static List<MenuVo> toMenuTreeVo(@Nullable List<Permission> permissions) {
List<MenuVo> list = Commons.transform(
permissions,
MenuVo.class,
(source, target) -> {
target.setMeta(source.getMetaJson());
target.setName(source.getUid());
},
Permission.Fields.meta
);
// 返回的树
ArrayList<MenuVo> roots = new ArrayList<>();
// 将数据添加到 Map
Map<Long, MenuVo> map = list.stream().collect(Collectors.toMap(MenuVo::getId, Function.identity()));
list.forEach(entity -> {
MenuVo vo = map.get(entity.getPid());
if (null == vo) {
roots.add(map.get(entity.getId()));
} else {
MenuVo parent = map.get(entity.getPid());
parent.getChildren().add(entity);
}
});
return roots;
}
/**
* 将权限集合转换成 Vo 集合
* @param permissions 权限集合
* @return vos
......@@ -49,6 +88,7 @@ public class Permissions {
.map(permission -> {
PermissionVo vo = new PermissionVo();
BeanUtils.copyProperties(permission, vo);
vo.setMeta(permission.getMetaJson());
return vo;
})
.collect(Collectors.toList());
......@@ -82,6 +122,7 @@ public class Permissions {
.map(permission -> {
PermissionVo vo = new PermissionVo();
BeanUtils.copyProperties(permission, vo);
vo.setMeta(permission.getMetaJson());
vo.setChildren(toTree(permissions, permission.getId()));
return vo;
})
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.auth.vo.auth;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
......@@ -25,6 +27,10 @@ public class LoginVo implements Serializable {
private static final long serialVersionUID = -8690942241103456896L;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "主键", example = "1")
Long userId;
@ApiModelProperty(value = "token", example = "c68ca9c8c04b4a59afeafd2fb7c04741")
String token;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 菜单输出类
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel("MenuVo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class MenuVo implements Serializable {
private static final long serialVersionUID = -9139328772148985141L;
@JsonIgnore
Long id;
@JsonIgnore
Long pid;
@ApiModelProperty(value = "标识", example = "home")
String name;
@ApiModelProperty(value = "路径", example = "/")
String path;
@ApiModelProperty(value = "组件", example = "/home")
String component;
@ApiModelProperty(value = "权限类型", example = "MENU")
Permission.Type type;
@ApiModelProperty(value = "元数据", example = "{}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
List<MenuVo> children;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.permission;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
......@@ -64,6 +65,9 @@ public class PermissionVo implements Serializable {
@ApiModelProperty(value = "父级ID", example = "0")
Long pid;
@ApiModelProperty(value = "元数据", example = "{}")
JSONObject meta;
@ApiModelProperty(value = "子权限")
List<PermissionVo> children;
}
......@@ -3,7 +3,6 @@ package com.yiring.auth.vo.user;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.yiring.auth.vo.permission.PermissionVo;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
......@@ -39,29 +38,16 @@ public class UserInfoVo implements Serializable {
@ApiModelProperty(value = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "手机号", example = "13012345678")
String mobile;
@ApiModelProperty(value = "介绍", example = "系统管理员")
String desc;
@ApiModelProperty(value = "邮箱", example = "developer@yiring.com")
String email;
@ApiModelProperty(value = "职称", example = "系统管理员")
String title;
@ApiModelProperty(value = "头像", example = "http://img.ifzm.cn/cat.jpg")
@ApiModelProperty(value = "头像", example = "https://files.catbox.moe/96nu4q.jpg")
String avatar;
@ApiModelProperty("角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty("权限")
@Builder.Default
List<PermissionVo> permissions = new ArrayList<>(0);
/**
* 通常用于前端决定登录成功后跳转到哪个模块页面
*/
@ApiModelProperty(value = "主页地址", example = "/")
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
String homePath;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.vo.user;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.yiring.auth.vo.role.RoleVo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 用户信息
* @author ifzm
* 2022/03/03 10:35
**/
@ApiModel("UserInfo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserMenuListVo implements Serializable {
private static final long serialVersionUID = -5319037883240327088L;
@JsonSerialize(using = ToStringSerializer.class)
@ApiModelProperty(value = "主键", example = "1")
Long userId;
@ApiModelProperty(value = "真实姓名", example = "超级用户")
String realName;
@ApiModelProperty(value = "用户名", example = "admin")
String username;
@ApiModelProperty(value = "介绍", example = "系统管理员")
String desc;
@ApiModelProperty(value = "头像", example = "https://files.catbox.moe/96nu4q.jpg")
String avatar;
@ApiModelProperty("角色")
@Builder.Default
List<RoleVo> roles = new ArrayList<>(0);
@ApiModelProperty(value = "用户主页", example = "/dashboard/workbench")
String homePath;
}
......@@ -45,7 +45,7 @@ public class UserVo implements Serializable {
@ApiModelProperty(value = "职称", example = "系统管理员")
String title;
@ApiModelProperty(value = "头像", example = "http://img.ifzm.cn/cat.jpg")
@ApiModelProperty(value = "头像", example = "https://files.catbox.moe/96nu4q.jpg")
String avatar;
@ApiModelProperty(value = "是否启用", example = "true")
......
......@@ -70,7 +70,7 @@ public class AuthController {
// 构建用户信息写入数据库
User user = User
.builder()
.title(param.getTitle())
.introduction(param.getIntroduction())
.avatar(param.getAvatar())
.mobile(param.getMobile())
.realName(param.getRealName())
......@@ -120,7 +120,7 @@ public class AuthController {
userRepository.saveAndFlush(user);
// 构建用户所需信息
LoginVo vo = LoginVo.builder().token(StpUtil.getTokenValue()).build();
LoginVo vo = LoginVo.builder().userId(user.getId()).token(StpUtil.getTokenValue()).build();
return Result.ok(vo);
}
......
......@@ -2,21 +2,25 @@
package com.yiring.auth.web.user;
import cn.dev33.satoken.stp.StpUtil;
import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.role.Role;
import com.yiring.auth.domain.role.RoleRepository;
import com.yiring.auth.domain.user.User;
import com.yiring.auth.domain.user.UserRepository;
import com.yiring.auth.param.IdsParam;
import com.yiring.auth.util.Permissions;
import com.yiring.auth.vo.permission.MenuVo;
import com.yiring.auth.vo.user.UserInfoVo;
import com.yiring.auth.vo.user.UserVo;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.FailStatusException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.vo.PageVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
......@@ -54,30 +58,30 @@ public class UserController {
RoleRepository roleRepository;
@ApiOperation(value = "获取登录用户信息")
@GetMapping("info")
public Result<UserInfoVo> info() {
Long id = StpUtil.getLoginIdAsLong();
Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) {
StpUtil.logout(id);
return Result.no(Status.UNAUTHORIZED);
}
User user = optional.get();
@GetMapping("getUserInfo")
public Result<UserInfoVo> getUserInfo() {
User user = getLoginUser();
UserInfoVo userInfoVo = UserInfoVo
.builder()
.userId(user.getId())
.username(user.getUsername())
.realName(user.getRealName())
.avatar(user.getAvatar())
.title(user.getTitle())
.desc(user.getIntroduction())
.roles(Permissions.toRoleVos(user.getRoles()))
.permissions(Permissions.toTree(Permissions.toPermissions(user.getRoles()), 0L))
// 默认跳转到用户看板
.homePath("/dashboard/workbench")
.build();
return Result.ok(userInfoVo);
}
@ApiOperation(value = "获取用户权限")
@GetMapping("getMenuList")
public Result<ArrayList<MenuVo>> getMenuList() {
User user = getLoginUser();
List<Permission> permissions = Permissions.toPermissions(user.getRoles());
List<MenuVo> vos = Permissions.toMenuTreeVo(permissions);
return Result.ok((ArrayList<MenuVo>) vos);
}
@ApiOperation(value = "分配角色")
@PostMapping("/manage/assign")
public Result<String> assign(@Valid IdParam idParam, @Valid IdsParam idsParam) {
......@@ -115,4 +119,19 @@ public class UserController {
PageVo<UserVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
/**
* 获取登录用户信息
* @return 用户信息
*/
private User getLoginUser() {
Long id = StpUtil.getLoginIdAsLong();
Optional<User> optional = userRepository.findById(id);
if (optional.isPresent()) {
return optional.get();
}
StpUtil.logout(id);
throw new FailStatusException(Status.UNAUTHORIZED);
}
}
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
token-name: Authorization
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
......
......@@ -8,9 +8,11 @@ dependencies {
// swagger annotations
implementation "io.swagger:swagger-annotations:${swaggerAnnotationsVersion}"
implementation "org.hibernate.validator:hibernate-validator:${hibernateValidatorVersion}"
// hutool-extra
implementation "cn.hutool:hutool-extra:${hutoolVersion}"
// hutool-json
implementation "cn.hutool:hutool-json:${hutoolVersion}"
// fastjson
implementation "com.alibaba:fastjson:${fastJsonVersion}"
}
......@@ -2,7 +2,7 @@
package com.yiring.common.aspect;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONObject;
import com.alibaba.fastjson.JSONObject;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Result;
import com.yiring.common.util.Commons;
......@@ -52,8 +52,8 @@ public class RequestAspect {
// Print Request Log (Optional Replace: MDC)
String extra = "";
if (Boolean.TRUE.equals(debug)) {
extra += String.format("\nHeaders: %s", new JSONObject(ServletUtil.getHeaderMap(request)).toStringPretty());
extra += String.format("\nParams: %s", new JSONObject(ServletUtil.getParamMap(request)).toStringPretty());
extra += String.format("\nHeaders: %s", JSONObject.toJSONString(ServletUtil.getHeaderMap(request), true));
extra += String.format("\nParams: %s", JSONObject.toJSONString(ServletUtil.getParamMap(request), true));
if (result instanceof Result) {
extra +=
String.format(
......
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.exception;
import com.yiring.common.core.Status;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.FieldDefaults;
/**
* Status 异常(用于快速失败,会进行全局异常拦截)
*
* @author Jim
* @version 0.1
* 2022/3/28 11:36
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class FailStatusException extends RuntimeException {
private static final long serialVersionUID = -4226669531686389671L;
/**
* 状态
*/
Status status;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.util;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
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;
/**
* 公共工具类
......@@ -15,6 +17,7 @@ import lombok.experimental.UtilityClass;
*/
@SuppressWarnings({ "unused" })
@Slf4j
@UtilityClass
public class Commons {
......@@ -79,4 +82,63 @@ public class Commons {
public boolean isNullOrEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合
* @param type 目标类型
* @param ignoreProperties 忽略属性
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @return 目标集合
*/
public <T, S> List<T> transform(@NonNull List<S> list, Class<T> type, String... ignoreProperties) {
return transform(list, type, null, ignoreProperties);
}
/**
* 将集合通过 BeanUtils 反射转换成指定类型集合
* @param list 原始数据集合
* @param type 目标类型
* @param fn 自定义处理函数
* @param ignoreProperties 忽略属性
* @param <T> 目标类型集合
* @param <S> 原类型集合
* @return 目标集合
*/
public <T, S> List<T> transform(
@NonNull List<S> list,
Class<T> type,
CallbackFunction<S, T> fn,
String... ignoreProperties
) {
try {
Constructor<T> declaredConstructor = type.getDeclaredConstructor();
List<T> targets = new ArrayList<>();
for (S source : list) {
// 实例化
T target = declaredConstructor.newInstance();
// 使用 BeanUtils 进行数据拷贝
BeanUtils.copyProperties(source, target, ignoreProperties);
// 通过自定义实现补充转换
if (fn != null) {
fn.apply(source, target);
}
targets.add(target);
}
return targets;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@FunctionalInterface
public interface CallbackFunction<S, T> {
void apply(S s, T t);
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论