diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java b/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java new file mode 100644 index 000000000..670029504 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/ErrorCode.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.common.exception; + +import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999],参见 {@link GlobalException} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + * + * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String message; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java b/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java new file mode 100644 index 000000000..d4f9c945e --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/GlobalException.java @@ -0,0 +1,41 @@ +package cn.iocoder.dashboard.common.exception; + +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 全局异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GlobalException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public GlobalException() { + } + + public GlobalException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + } + + public GlobalException(Integer code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java b/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java new file mode 100644 index 000000000..dec24db48 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/ServiceException.java @@ -0,0 +1,59 @@ +package cn.iocoder.dashboard.common.exception; + +import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMessage(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java b/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 000000000..e215f2bc0 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.common.exception.enums; + +import cn.iocoder.dashboard.common.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + * + * @author 芋道源码 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + + static boolean isMatch(Integer code) { + return code != null + && code >= SUCCESS.getCode() && code <= UNKNOWN.getCode(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java b/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 000000000..be25c7062 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.common.exception.enums; + +/** + * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * + * 一共 10 位,分成四段 + * + * 第一段,1 位,类型 + * 1 - 业务级别异常 + * x - 预留 + * 第二段,3 位,系统类型 + * 001 - 用户系统 + * 002 - 商品系统 + * 003 - 订单系统 + * 004 - 支付系统 + * 005 - 优惠劵系统 + * ... - ... + * 第三段,3 位,模块 + * 不限制规则。 + * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: + * 001 - OAuth2 模块 + * 002 - User 模块 + * 003 - MobileCode 模块 + * 第四段,3 位,错误码 + * 不限制规则。 + * 一般建议,每个模块自增。 + * + * @author 芋道源码 + */ +public class ServiceErrorCodeRange { + + // 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000] + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java b/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 000000000..19d6763c1 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,122 @@ +package cn.iocoder.dashboard.common.exception.util; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.ServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +public class ServiceExceptionUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceExceptionUtil.class); + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMessage()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + private static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + LOGGER.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i, messagePattern.length())); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern.substring(i, j)); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + LOGGER.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i, messagePattern.length())); + return sbuf.toString(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/package-info.java b/src/main/java/cn/iocoder/dashboard/common/package-info.java new file mode 100644 index 000000000..79cbafd83 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package cn.iocoder.dashboard.common; diff --git a/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java b/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java new file mode 100644 index 000000000..030d9df67 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/pojo/CommonResult.java @@ -0,0 +1,106 @@ +package cn.iocoder.dashboard.common.pojo; + +import cn.iocoder.dashboard.common.exception.ErrorCode; +import cn.iocoder.dashboard.common.exception.GlobalException; +import cn.iocoder.dashboard.common.exception.ServiceException; +import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.springframework.util.Assert; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public final class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMessage() () + */ + private String msg; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMessage()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + @JSONField(serialize = false) // 避免序列化 + public boolean isSuccess() { + return GlobalErrorCodeConstants.SUCCESS.getCode().equals(code); + } + + @JSONField(serialize = false) // 避免序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link GlobalException} 或 {@link ServiceException} 异常 + */ + public void checkError() throws GlobalException, ServiceException { + if (isSuccess()) { + return; + } + // 全局异常 + if (GlobalErrorCodeConstants.isMatch(code)) { + throw new GlobalException(code, msg); + } + // 业务异常 + throw new ServiceException(code, msg); + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + + public static CommonResult error(GlobalException globalException) { + return error(globalException.getCode(), globalException.getMessage()); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java b/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java new file mode 100644 index 000000000..690a2ab4c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/pojo/PageParam.java @@ -0,0 +1,26 @@ +package cn.iocoder.dashboard.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@ApiModel("分页参数") +@Data +public class PageParam implements Serializable { + + @ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1") + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo; + + @ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10") + @NotNull(message = "每页条数不能为空") + @Range(min = 1, max = 100, message = "条数范围为 [1, 100]") + private Integer pageSize; + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java b/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java new file mode 100644 index 000000000..182ebad1f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/pojo/PageResult.java @@ -0,0 +1,20 @@ +package cn.iocoder.dashboard.common.pojo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +@ApiModel("分页结果") +@Data +public final class PageResult implements Serializable { + + @ApiModelProperty(value = "数据", required = true) + private List list; + + @ApiModelProperty(value = "总量", required = true) + private Long total; + +} diff --git a/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java b/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java new file mode 100644 index 000000000..118a91b88 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/common/pojo/SortingField.java @@ -0,0 +1,56 @@ +package cn.iocoder.dashboard.common.pojo; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +public class SortingField implements Serializable { + + /** + * 顺序 - 升序 + */ + public static final String ORDER_ASC = "asc"; + /** + * 顺序 - 降序 + */ + public static final String ORDER_DESC = "desc"; + + /** + * 字段 + */ + private String field; + /** + * 顺序 + */ + private String order; + + // 空构造方法,解决反序列化 + public SortingField() { + } + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java b/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java new file mode 100644 index 000000000..d15a1ff0b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/util/collection/CollectionUtils.java @@ -0,0 +1,66 @@ +package cn.iocoder.dashboard.util.collection; + +import cn.hutool.core.collection.CollectionUtil; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Collection 工具类 + * + * @author 芋道源码 + */ +public class CollectionUtils { + + public static Set asSet(T... objs) { + return new HashSet<>(Arrays.asList(objs)); + } + + public static List convertList(List from, Function func) { + return from.stream().map(func).collect(Collectors.toList()); + } + + public static Set convertSet(List from, Function func) { + return from.stream().map(func).collect(Collectors.toSet()); + } + + public static Map convertMap(List from, Function keyFunc) { + return from.stream().collect(Collectors.toMap(keyFunc, item -> item)); + } + + public static Map convertMap(List from, Function keyFunc, Function valueFunc) { + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc)); + } + + public static Map> convertMultiMap(List from, Function keyFunc) { + return from.stream().collect(Collectors.groupingBy(keyFunc, + Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(List from, Function keyFunc, Function valueFunc) { + return from.stream().collect(Collectors.groupingBy(keyFunc, + Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(List from, Function keyFunc, Function valueFunc) { + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java b/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java new file mode 100644 index 000000000..da54d431c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/util/collection/MapUtils.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Map 工具类 + * + * @author 芋道源码 + */ +public class MapUtils { + + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/util/package-info.java b/src/main/java/cn/iocoder/dashboard/util/package-info.java new file mode 100644 index 000000000..dd564c247 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package cn.iocoder.dashboard.util;