mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-07 23:55:07 +08:00
再次调整项目结构 x 2
This commit is contained in:
@ -1,15 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.core;
|
||||
|
||||
/**
|
||||
* 可生成 Int 数组的接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IntArrayValuable {
|
||||
|
||||
/**
|
||||
* @return int 数组
|
||||
*/
|
||||
int[] array();
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.core;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Key Value 的键值对
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeyValue<K, V> {
|
||||
|
||||
private K key;
|
||||
private V value;
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CommonStatusEnum {
|
||||
|
||||
ENABLE(0, "开启"),
|
||||
DISABLE(1, "关闭");
|
||||
|
||||
/**
|
||||
* 状态值
|
||||
*/
|
||||
private final Integer status;
|
||||
/**
|
||||
* 状态名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 通用状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DefaultBitFieldEnum {
|
||||
|
||||
NO(0, "否"),
|
||||
YES(1, "是");
|
||||
|
||||
/**
|
||||
* 状态值
|
||||
*/
|
||||
private final Integer val;
|
||||
/**
|
||||
* 状态名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 全局用户类型枚举
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum UserTypeEnum {
|
||||
|
||||
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
||||
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer value;
|
||||
/**
|
||||
* 类型名
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.exception;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.common.exception.enums.ServiceErrorCodeRange;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 错误码对象
|
||||
*
|
||||
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants}
|
||||
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange}
|
||||
*
|
||||
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备
|
||||
*/
|
||||
@Data
|
||||
public class ErrorCode {
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
/**
|
||||
* 错误提示
|
||||
*/
|
||||
private final String msg;
|
||||
|
||||
public ErrorCode(Integer code, String message) {
|
||||
this.code = code;
|
||||
this.msg = message;
|
||||
}
|
||||
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
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.getMsg();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public ServiceException setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
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 LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
|
||||
ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
|
||||
|
||||
// ========== 服务端错误段 ==========
|
||||
|
||||
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
|
||||
|
||||
// ========== 自定义错误段 ==========
|
||||
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
|
||||
|
||||
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
|
||||
|
||||
static boolean isMatch(Integer code) {
|
||||
return code != null
|
||||
&& code >= SUCCESS.getCode() && code <= UNKNOWN.getCode();
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
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]
|
||||
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.exception.util;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
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<Integer, String> MESSAGES = new ConcurrentHashMap<>();
|
||||
|
||||
public static void putAll(Map<Integer, String> 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.getMsg());
|
||||
return exception0(errorCode.getCode(), messagePattern);
|
||||
}
|
||||
|
||||
public static ServiceException exception(ErrorCode errorCode, Object... params) {
|
||||
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg());
|
||||
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 格式化后的提示
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public 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));
|
||||
return sbuf.toString();
|
||||
}
|
||||
} else {
|
||||
sbuf.append(messagePattern, 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));
|
||||
return sbuf.toString();
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 基础的通用类,和框架无关
|
||||
*
|
||||
* 例如说,CommonResult 为通用返回
|
||||
*/
|
||||
package cn.iocoder.dashboard.common;
|
@ -1,102 +0,0 @@
|
||||
package cn.iocoder.dashboard.common.pojo;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 通用返回
|
||||
*
|
||||
* @param <T> 数据泛型
|
||||
*/
|
||||
@Data
|
||||
public class CommonResult<T> implements Serializable {
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*
|
||||
* @see ErrorCode#getCode()
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
/**
|
||||
* 错误提示,用户可阅读
|
||||
*
|
||||
* @see ErrorCode#getMsg() ()
|
||||
*/
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 将传入的 result 对象,转换成另外一个泛型结果的对象
|
||||
*
|
||||
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。
|
||||
*
|
||||
* @param result 传入的 result 对象
|
||||
* @param <T> 返回的泛型
|
||||
* @return 新的 CommonResult 对象
|
||||
*/
|
||||
public static <T> CommonResult<T> error(CommonResult<?> result) {
|
||||
return error(result.getCode(), result.getMsg());
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(Integer code, String message) {
|
||||
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!");
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = code;
|
||||
result.msg = message;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ErrorCode errorCode) {
|
||||
return error(errorCode.getCode(), errorCode.getMsg());
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> success(T data) {
|
||||
CommonResult<T> result = new CommonResult<>();
|
||||
result.code = GlobalErrorCodeConstants.SUCCESS.getCode();
|
||||
result.data = data;
|
||||
result.msg = "";
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean isSuccess(Integer code) {
|
||||
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode());
|
||||
}
|
||||
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public boolean isSuccess() {
|
||||
return isSuccess(code);
|
||||
}
|
||||
|
||||
@JsonIgnore // 避免 jackson 序列化
|
||||
public boolean isError() {
|
||||
return !isSuccess();
|
||||
}
|
||||
|
||||
// ========= 和 Exception 异常体系集成 =========
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常
|
||||
*/
|
||||
public void checkError() throws ServiceException {
|
||||
if (isSuccess()) {
|
||||
return;
|
||||
}
|
||||
// 业务异常
|
||||
throw new ServiceException(code, msg);
|
||||
}
|
||||
|
||||
public static <T> CommonResult<T> error(ServiceException serviceException) {
|
||||
return error(serviceException.getCode(), serviceException.getMessage());
|
||||
}
|
||||
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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 {
|
||||
|
||||
private static final Integer PAGE_NO = 1;
|
||||
private static final Integer PAGE_SIZE = 10;
|
||||
|
||||
@ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1")
|
||||
@NotNull(message = "页码不能为空")
|
||||
@Min(value = 1, message = "页码最小值为 1")
|
||||
private Integer pageNo = PAGE_NO;
|
||||
|
||||
@ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10")
|
||||
@NotNull(message = "每页条数不能为空")
|
||||
@Range(min = 1, max = 100, message = "条数范围为 [1, 100]")
|
||||
private Integer pageSize = PAGE_SIZE;
|
||||
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ApiModel("分页结果")
|
||||
@Data
|
||||
public final class PageResult<T> implements Serializable {
|
||||
|
||||
@ApiModelProperty(value = "数据", required = true)
|
||||
private List<T> list;
|
||||
|
||||
@ApiModelProperty(value = "总量", required = true)
|
||||
private Long total;
|
||||
|
||||
public PageResult() {
|
||||
}
|
||||
|
||||
public PageResult(List<T> list, Long total) {
|
||||
this.list = list;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public PageResult(Long total) {
|
||||
this.list = new ArrayList<>();
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> empty() {
|
||||
return new PageResult<>(0L);
|
||||
}
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.core;
|
||||
|
||||
/**
|
||||
* 针对 {@link com.ctrip.framework.apollo.core.ConfigConsts} 的补充,主要增加:
|
||||
*
|
||||
* 1. apollo.jdbc.* 配置项的枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ConfigConsts {
|
||||
|
||||
public static final String APOLLO_JDBC_URL = "apollo.jdbc.url";
|
||||
public static final String APOLLO_JDBC_USERNAME = "apollo.jdbc.username";
|
||||
public static final String APOLLO_JDBC_PASSWORD = "apollo.jdbc.password";
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.internals;
|
||||
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 配置 Framework DAO 接口
|
||||
*/
|
||||
public interface ConfigFrameworkDAO {
|
||||
|
||||
/**
|
||||
* 查询是否存在比 maxUpdateTime 更新记录更晚的配置
|
||||
*
|
||||
* @param maxUpdateTime 最大更新时间
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime);
|
||||
|
||||
/**
|
||||
* 查询配置列表
|
||||
*
|
||||
* @return 配置列表
|
||||
*/
|
||||
List<InfConfigDO> selectList();
|
||||
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.internals;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts;
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.mysql.config.InfConfigDAOImpl;
|
||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
|
||||
import com.ctrip.framework.apollo.Apollo;
|
||||
import com.ctrip.framework.apollo.build.ApolloInjector;
|
||||
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
|
||||
import com.ctrip.framework.apollo.enums.ConfigSourceType;
|
||||
import com.ctrip.framework.apollo.internals.AbstractConfigRepository;
|
||||
import com.ctrip.framework.apollo.internals.ConfigRepository;
|
||||
import com.ctrip.framework.apollo.tracer.Tracer;
|
||||
import com.ctrip.framework.apollo.util.ConfigUtil;
|
||||
import com.ctrip.framework.apollo.util.factory.PropertiesFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
@Slf4j
|
||||
public class DBConfigRepository extends AbstractConfigRepository {
|
||||
|
||||
private final static ScheduledExecutorService m_executorService;
|
||||
|
||||
private static DBConfigRepository INSTANCE;
|
||||
|
||||
static {
|
||||
m_executorService = Executors.newScheduledThreadPool(1,
|
||||
ApolloThreadFactory.create(DBConfigRepository.class.getSimpleName(), true));
|
||||
}
|
||||
|
||||
private final ConfigUtil m_configUtil;
|
||||
private final PropertiesFactory propertiesFactory;
|
||||
private final String m_namespace;
|
||||
|
||||
/**
|
||||
* 配置缓存,使用 Properties 存储
|
||||
*/
|
||||
private volatile Properties m_configCache;
|
||||
/**
|
||||
* 缓存配置的最大更新时间,用于后续的增量轮询,判断是否有更新
|
||||
*/
|
||||
private volatile Date maxUpdateTime;
|
||||
|
||||
/**
|
||||
* 配置读取 DAO
|
||||
*/
|
||||
private final ConfigFrameworkDAO configFrameworkDAO;
|
||||
|
||||
public DBConfigRepository(String namespace) {
|
||||
// 初始化变量
|
||||
this.m_namespace = namespace;
|
||||
this.propertiesFactory = ApolloInjector.getInstance(PropertiesFactory.class);
|
||||
this.m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
|
||||
// 初始化 DB
|
||||
this.configFrameworkDAO = new InfConfigDAOImpl(System.getProperty(ConfigConsts.APOLLO_JDBC_URL),
|
||||
System.getProperty(ConfigConsts.APOLLO_JDBC_USERNAME), System.getProperty(ConfigConsts.APOLLO_JDBC_PASSWORD));
|
||||
|
||||
// 初始化加载
|
||||
this.trySync();
|
||||
// 初始化定时任务
|
||||
this.schedulePeriodicRefresh();
|
||||
|
||||
// 设置单例
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知同步,
|
||||
*/
|
||||
public static void noticeSync() {
|
||||
// 提交到线程池中,避免和 schedulePeriodicRefresh 并发问题
|
||||
m_executorService.submit(() -> {
|
||||
INSTANCE.trySync();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sync() {
|
||||
// 第一步,尝试获取配置
|
||||
List<InfConfigDO> configs = this.loadConfigIfUpdate(this.maxUpdateTime);
|
||||
if (CollUtil.isEmpty(configs)) { // 如果没有更新,则返回
|
||||
return;
|
||||
}
|
||||
|
||||
// 第二步,构建新的 Properties
|
||||
Properties newProperties = this.buildProperties(configs);
|
||||
this.m_configCache = newProperties;
|
||||
// 第三步,获取最大的配置时间
|
||||
assert configs.size() > 0; // 断言,避免告警
|
||||
this.maxUpdateTime = configs.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
|
||||
// 第四部,触发配置刷新!重要!!!!
|
||||
super.fireRepositoryChange(m_namespace, newProperties);
|
||||
log.info("[sync][缓存配置,数量为:{}]", configs.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Properties getConfig() {
|
||||
// 兜底,避免可能存在配置为 null 的情况
|
||||
if (m_configCache == null) {
|
||||
this.trySync();
|
||||
}
|
||||
// 返回配置
|
||||
return m_configCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) {
|
||||
// 啥事不做
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigSourceType getSourceType() {
|
||||
return ConfigSourceType.REMOTE;
|
||||
}
|
||||
|
||||
private Properties buildProperties(List<InfConfigDO> configs) {
|
||||
Properties properties = propertiesFactory.getPropertiesInstance();
|
||||
configs.stream().filter(BaseDO::getDeleted) // 过滤掉被删除的配置
|
||||
.forEach(config -> properties.put(config.getKey(), config.getValue()));
|
||||
return properties;
|
||||
}
|
||||
|
||||
// ========== 定时器相关操作 ==========
|
||||
|
||||
private void schedulePeriodicRefresh() {
|
||||
log.debug("Schedule periodic refresh with interval: {} {}",
|
||||
m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit());
|
||||
m_executorService.scheduleAtFixedRate(() -> {
|
||||
Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace));
|
||||
log.debug("refresh config for namespace: {}", m_namespace);
|
||||
|
||||
// 执行同步. 内部已经 try catch 掉异常,无需在处理
|
||||
trySync();
|
||||
|
||||
Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION);
|
||||
}, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(),
|
||||
m_configUtil.getRefreshIntervalTimeUnit());
|
||||
}
|
||||
|
||||
// ========== 数据库相关操作 ==========
|
||||
|
||||
/**
|
||||
* 如果配置发生变化,从数据库中获取最新的全量配置。
|
||||
* 如果未发生变化,则返回空
|
||||
*
|
||||
* @param maxUpdateTime 当前配置的最大更新时间
|
||||
* @return 配置列表
|
||||
*/
|
||||
private List<InfConfigDO> loadConfigIfUpdate(Date maxUpdateTime) {
|
||||
// 第一步,判断是否要更新。
|
||||
if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
|
||||
log.info("[loadConfigIfUpdate][首次加载全量配置]");
|
||||
} else { // 判断数据库中是否有更新的配置
|
||||
if (!configFrameworkDAO.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
|
||||
return null;
|
||||
}
|
||||
log.info("[loadConfigIfUpdate][增量加载全量配置]");
|
||||
}
|
||||
// 第二步,如果有更新,则从数据库加载所有配置
|
||||
return configFrameworkDAO.selectList();
|
||||
}
|
||||
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.internals;
|
||||
|
||||
import cn.iocoder.dashboard.framework.apollo.spi.DBConfigFactory;
|
||||
import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
|
||||
import com.ctrip.framework.apollo.internals.*;
|
||||
import com.ctrip.framework.apollo.spi.*;
|
||||
import com.ctrip.framework.apollo.tracer.Tracer;
|
||||
import com.ctrip.framework.apollo.util.ConfigUtil;
|
||||
import com.ctrip.framework.apollo.util.factory.DefaultPropertiesFactory;
|
||||
import com.ctrip.framework.apollo.util.factory.PropertiesFactory;
|
||||
import com.ctrip.framework.apollo.util.http.HttpUtil;
|
||||
import com.ctrip.framework.apollo.util.yaml.YamlParser;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
/**
|
||||
* Guice injector
|
||||
*
|
||||
* 基于 Guice 注入器实现类
|
||||
*
|
||||
* @author Jason Song(song_s@ctrip.com)
|
||||
*/
|
||||
public class DefaultXInjector implements Injector {
|
||||
|
||||
private final com.google.inject.Injector m_injector;
|
||||
|
||||
public DefaultXInjector() {
|
||||
try {
|
||||
m_injector = Guice.createInjector(new ApolloModule());
|
||||
} catch (Throwable ex) {
|
||||
ApolloConfigException exception = new ApolloConfigException("Unable to initialize Guice Injector!", ex);
|
||||
Tracer.logError(exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInstance(Class<T> clazz) {
|
||||
try {
|
||||
return m_injector.getInstance(clazz);
|
||||
} catch (Throwable ex) {
|
||||
Tracer.logError(ex);
|
||||
throw new ApolloConfigException(String.format("Unable to load instance for %s!", clazz.getName()), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getInstance(Class<T> clazz, String name) {
|
||||
// Guice does not support get instance by type and name
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class ApolloModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ConfigManager.class).to(DefaultConfigManager.class).in(Singleton.class);
|
||||
bind(ConfigFactoryManager.class).to(DefaultConfigFactoryManager.class).in(Singleton.class);
|
||||
bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class);
|
||||
|
||||
// 自定义 ConfigFactory 实现,使用 DB 作为数据源
|
||||
bind(ConfigFactory.class).to(DBConfigFactory.class).in(Singleton.class);
|
||||
|
||||
bind(ConfigUtil.class).in(Singleton.class);
|
||||
bind(HttpUtil.class).in(Singleton.class);
|
||||
bind(ConfigServiceLocator.class).in(Singleton.class);
|
||||
bind(RemoteConfigLongPollService.class).in(Singleton.class);
|
||||
bind(YamlParser.class).in(Singleton.class);
|
||||
bind(PropertiesFactory.class).to(DefaultPropertiesFactory.class).in(Singleton.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
/**
|
||||
* 配置中心客户端,基于 Apollo Client 进行简化
|
||||
*
|
||||
* 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO} 表作为配置源。
|
||||
* 当然,功能肯定也会相对少些,满足最小化诉求。
|
||||
*
|
||||
* 1. 项目初始化时,可以使用 SysConfigDO 表的配置
|
||||
* 2. 使用 Spring @Value 可以注入属性
|
||||
* 3. SysConfigDO 表的配置修改时,注入到 @Value 的属性可以刷新
|
||||
*
|
||||
* 另外,整个包结构会参考 Apollo 为主,方便维护与理解
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.apollo;
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.spi;
|
||||
|
||||
import cn.iocoder.dashboard.framework.apollo.internals.DBConfigRepository;
|
||||
import com.ctrip.framework.apollo.Config;
|
||||
import com.ctrip.framework.apollo.ConfigFile;
|
||||
import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
|
||||
import com.ctrip.framework.apollo.internals.ConfigRepository;
|
||||
import com.ctrip.framework.apollo.internals.DefaultConfig;
|
||||
import com.ctrip.framework.apollo.spi.ConfigFactory;
|
||||
|
||||
/**
|
||||
* 基于 DB 的 ConfigFactory 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DBConfigFactory implements ConfigFactory {
|
||||
|
||||
@Override
|
||||
public Config create(String namespace) {
|
||||
return new DefaultConfig(namespace, this.createDBConfigRepository(namespace));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigFile createConfigFile(String namespace, ConfigFileFormat configFileFormat) {
|
||||
throw new UnsupportedOperationException("暂不支持 Apollo 配置文件");
|
||||
}
|
||||
|
||||
private ConfigRepository createDBConfigRepository(String namespace) {
|
||||
return new DBConfigRepository(namespace);
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.apollo.spring.boot;
|
||||
|
||||
import cn.iocoder.dashboard.framework.apollo.core.ConfigConsts;
|
||||
import com.google.common.base.Strings;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.env.EnvironmentPostProcessor;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
/**
|
||||
* 对 {@link com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer} 的补充,目前的目的有:
|
||||
*
|
||||
* 1. 将自定义的 apollo.jdbc 设置到 System 变量中
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ApolloApplicationContextInitializer implements EnvironmentPostProcessor, Ordered {
|
||||
|
||||
/**
|
||||
* 优先级更高,要早于 Apollo 的 ApolloApplicationContextInitializer 的初始化
|
||||
*/
|
||||
public static final int DEFAULT_ORDER = -1;
|
||||
|
||||
private int order = DEFAULT_ORDER;
|
||||
|
||||
private static final String[] APOLLO_SYSTEM_PROPERTIES = {ConfigConsts.APOLLO_JDBC_URL,
|
||||
ConfigConsts.APOLLO_JDBC_USERNAME, ConfigConsts.APOLLO_JDBC_PASSWORD};
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
initializeSystemProperty(environment);
|
||||
}
|
||||
|
||||
/**
|
||||
* To fill system properties from environment config
|
||||
*/
|
||||
void initializeSystemProperty(ConfigurableEnvironment environment) {
|
||||
for (String propertyName : APOLLO_SYSTEM_PROPERTIES) {
|
||||
fillSystemPropertyFromEnvironment(environment, propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) {
|
||||
if (System.getProperty(propertyName) != null) {
|
||||
return;
|
||||
}
|
||||
String propertyValue = environment.getProperty(propertyName);
|
||||
if (Strings.isNullOrEmpty(propertyValue)) {
|
||||
return;
|
||||
}
|
||||
System.setProperty(propertyName, propertyValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.async.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfiguration {
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 异步执行,基于 Spring @Async 实现
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.async;
|
@ -1 +0,0 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Async-Job/?yudao>
|
@ -1,9 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.captcha.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(CaptchaProperties.class)
|
||||
public class CaptchaConfig {
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.captcha.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.captcha")
|
||||
@Validated
|
||||
@Data
|
||||
public class CaptchaProperties {
|
||||
|
||||
/**
|
||||
* 验证码的过期时间
|
||||
*/
|
||||
@NotNull(message = "验证码的过期时间不为空")
|
||||
private Duration timeout;
|
||||
/**
|
||||
* 验证码的高度
|
||||
*/
|
||||
@NotNull(message = "验证码的高度不能为空")
|
||||
private Integer height;
|
||||
/**
|
||||
* 验证码的宽度
|
||||
*/
|
||||
@NotNull(message = "验证码的宽度不能为空")
|
||||
private Integer width;
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 基于 Hutool captcha 库,实现验证码功能
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.captcha;
|
@ -1,9 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.codegen.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(CodegenProperties.class)
|
||||
public class CodegenConfiguration {
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.codegen.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Collection;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.codegen")
|
||||
@Validated
|
||||
@Data
|
||||
public class CodegenProperties {
|
||||
|
||||
/**
|
||||
* 生成的 Java 代码的基础包
|
||||
*/
|
||||
@NotNull(message = "Java 代码的基础包不能为空")
|
||||
private String basePackage;
|
||||
|
||||
/**
|
||||
* 数据库名数组
|
||||
*/
|
||||
@NotEmpty(message = "数据库不能为空")
|
||||
private Collection<String> dbSchemas;
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 代码生成器
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.codegen;
|
@ -1,38 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.datasource.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.datasource.core.filter.DruidAdRemoveFilter;
|
||||
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* 数据库配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理
|
||||
public class DataSourceConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true")
|
||||
public FilterRegistrationBean<DruidAdRemoveFilter> druidAdRemoveFilterFilter(DruidStatProperties properties) {
|
||||
// 获取 druid web 监控页面的参数
|
||||
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
|
||||
// 提取 common.js 的配置路径
|
||||
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
|
||||
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
|
||||
// 创建 DruidAdRemoveFilter Bean
|
||||
FilterRegistrationBean<DruidAdRemoveFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new DruidAdRemoveFilter());
|
||||
registrationBean.addUrlPatterns(commonJsPattern);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.datasource.core.enums;
|
||||
|
||||
/**
|
||||
* 对应于多数据源中不同数据源配置
|
||||
*
|
||||
* 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。
|
||||
* 注意,默认是 {@link #MASTER} 数据源
|
||||
*
|
||||
* 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html
|
||||
*/
|
||||
public interface DataSourceEnum {
|
||||
|
||||
/**
|
||||
* 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解
|
||||
*/
|
||||
String MASTER = "master";
|
||||
/**
|
||||
* 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解
|
||||
*/
|
||||
String SLAVE = "slave";
|
||||
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.datasource.core.filter;
|
||||
|
||||
import com.alibaba.druid.util.Utils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Druid 底部广告过滤器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DruidAdRemoveFilter extends OncePerRequestFilter {
|
||||
|
||||
/**
|
||||
* common.js 的路径
|
||||
*/
|
||||
private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
chain.doFilter(request, response);
|
||||
// 重置缓冲区,响应头不会被重置
|
||||
response.resetBuffer();
|
||||
// 获取 common.js
|
||||
String text = Utils.readFromResource(COMMON_JS_ILE_PATH);
|
||||
// 正则替换 banner, 除去底部的广告信息
|
||||
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
|
||||
text = text.replaceAll("powered.*?shrek.wang</a>", "");
|
||||
response.getWriter().write(text);
|
||||
}
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/dynamic-datasource/?yudao>
|
@ -1 +0,0 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao>
|
@ -1,18 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.dict.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.dict.core.util.DictUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class DictConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public DictUtils dictUtils(DictDataFrameworkService service) {
|
||||
DictUtils.init(service);
|
||||
return new DictUtils();
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.dict.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DictDataFrameworkService {
|
||||
|
||||
/**
|
||||
* 获得指定的字典数据,从缓存中
|
||||
*
|
||||
* @param type 字典类型
|
||||
* @param value 字典数据值
|
||||
* @return 字典数据
|
||||
*/
|
||||
SysDictDataDO getDictDataFromCache(String type, String value);
|
||||
|
||||
/**
|
||||
* 解析获得指定的字典数据,从缓存中
|
||||
*
|
||||
* @param type 字典类型
|
||||
* @param label 字典数据标签
|
||||
* @return 字典数据
|
||||
*/
|
||||
SysDictDataDO parseDictDataFromCache(String type, String label);
|
||||
|
||||
/**
|
||||
* 获得指定类型的字典数据,从缓存中
|
||||
*
|
||||
* @param type 字典类型
|
||||
* @return 字典数据列表
|
||||
*/
|
||||
List<SysDictDataDO> listDictDatasFromCache(String type);
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.dict.core.util;
|
||||
|
||||
import cn.iocoder.dashboard.framework.dict.core.service.DictDataFrameworkService;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 字典工具类
|
||||
*/
|
||||
@Slf4j
|
||||
public class DictUtils {
|
||||
|
||||
private static DictDataFrameworkService service;
|
||||
|
||||
public static void init(DictDataFrameworkService service) {
|
||||
DictUtils.service = service;
|
||||
log.info("[init][初始化 DictUtils 成功]");
|
||||
}
|
||||
|
||||
public static SysDictDataDO getDictDataFromCache(String type, String value) {
|
||||
return service.getDictDataFromCache(type, value);
|
||||
}
|
||||
|
||||
public static SysDictDataDO parseDictDataFromCache(String type, String label) {
|
||||
return service.getDictDataFromCache(type, label);
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类
|
||||
*
|
||||
* 通过将字典缓存在内存中,保证性能
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.dict;
|
@ -1,36 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGenerator;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoader;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.loader.ErrorCodeLoaderImpl;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 错误码配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(ErrorCodeProperties.class)
|
||||
@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码
|
||||
public class ErrorCodeConfiguration {
|
||||
|
||||
@Bean
|
||||
public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeProperties errorCodeProperties,
|
||||
ErrorCodeFrameworkService errorCodeFrameworkService) {
|
||||
return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(),
|
||||
errorCodeFrameworkService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName,
|
||||
ErrorCodeFrameworkService errorCodeFrameworkService) {
|
||||
return new ErrorCodeLoaderImpl(applicationName, errorCodeFrameworkService);
|
||||
}
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 错误码的配置属性类
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
@ConfigurationProperties("yudao.error-code")
|
||||
@Data
|
||||
@Validated
|
||||
public class ErrorCodeProperties {
|
||||
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
@NotNull(message = "错误码枚举类不能为空")
|
||||
private List<String> constantsClassList;
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 错误码自动生成 DTO
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ErrorCodeAutoGenerateReqDTO {
|
||||
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
/**
|
||||
* 错误码编码
|
||||
*/
|
||||
@NotNull(message = "错误码编码不能为空")
|
||||
private Integer code;
|
||||
/**
|
||||
* 错误码错误提示
|
||||
*/
|
||||
@NotEmpty(message = "错误码错误提示不能为空")
|
||||
private String message;
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 错误码的 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class ErrorCodeRespDTO {
|
||||
|
||||
/**
|
||||
* 错误码编码
|
||||
*/
|
||||
private Integer code;
|
||||
/**
|
||||
* 错误码错误提示
|
||||
*/
|
||||
private String message;
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Date updateTime;
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.generator;
|
||||
|
||||
/**
|
||||
* 错误码的自动生成器
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
public interface ErrorCodeAutoGenerator {
|
||||
|
||||
/**
|
||||
* 将配置类到错误码写入数据库
|
||||
*/
|
||||
void execute();
|
||||
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.generator;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.dashboard.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ErrorCodeAutoGenerator 的实现类
|
||||
* 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中
|
||||
*
|
||||
* @author dylan
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
|
||||
|
||||
/**
|
||||
* 应用分组
|
||||
*/
|
||||
private final String applicationName;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
private final List<String> constantsClassList;
|
||||
/**
|
||||
* 错误码 Service
|
||||
*/
|
||||
private final ErrorCodeFrameworkService errorCodeService;
|
||||
|
||||
@Override
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Async // 异步,保证项目的启动过程,毕竟非关键流程
|
||||
public void execute() {
|
||||
// 第一步,解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = parseErrorCode();
|
||||
log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
|
||||
|
||||
// 第二步,写入到 system 服务
|
||||
errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs);
|
||||
log.info("[execute][写入到 system 组件完成]");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 constantsClassList 变量,转换成错误码数组
|
||||
*
|
||||
* @return 错误码数组
|
||||
*/
|
||||
private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode() {
|
||||
// 校验 errorCodeConstantsClass 参数
|
||||
if (CollUtil.isEmpty(constantsClassList)) {
|
||||
log.info("[execute][未配置 yudao.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// 解析错误码
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
|
||||
constantsClassList.forEach(constantsClass -> {
|
||||
// 解析错误码枚举类
|
||||
Class<?> errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass);
|
||||
// 解析错误码
|
||||
autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz));
|
||||
});
|
||||
return autoGenerateDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析错误码类,获得错误码数组
|
||||
*
|
||||
* @return 错误码数组
|
||||
*/
|
||||
private List<ErrorCodeAutoGenerateReqDTO> parseErrorCode(Class<?> constantsClass) {
|
||||
List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs = new ArrayList<>();
|
||||
Arrays.stream(constantsClass.getFields()).forEach(field -> {
|
||||
if (field.getType() != ErrorCode.class) {
|
||||
return;
|
||||
}
|
||||
// 转换成 ErrorCodeAutoGenerateReqDTO 对象
|
||||
ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field);
|
||||
autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName)
|
||||
.setCode(errorCode.getCode()).setMessage(errorCode.getMsg()));
|
||||
});
|
||||
return autoGenerateDTOs;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.loader;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
|
||||
|
||||
/**
|
||||
* 错误码加载器
|
||||
*
|
||||
* 注意,错误码最终加载到 {@link cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil} 的 MESSAGES 变量中!
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
public interface ErrorCodeLoader {
|
||||
|
||||
/**
|
||||
* 添加错误码
|
||||
*
|
||||
* @param code 错误码的编号
|
||||
* @param msg 错误码的提示
|
||||
*/
|
||||
default void putErrorCode(Integer code, String msg) {
|
||||
ServiceExceptionUtil.put(code, msg);
|
||||
}
|
||||
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.loader;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.service.ErrorCodeFrameworkService;
|
||||
import cn.iocoder.dashboard.util.date.DateUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。
|
||||
*
|
||||
* 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。
|
||||
*
|
||||
* @author dlyan
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
|
||||
|
||||
/**
|
||||
* 刷新错误码的频率,单位:毫秒
|
||||
*/
|
||||
private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000;
|
||||
|
||||
/**
|
||||
* 应用分组
|
||||
*/
|
||||
private final String applicationName;
|
||||
/**
|
||||
* 错误码 Service
|
||||
*/
|
||||
private final ErrorCodeFrameworkService errorCodeService;
|
||||
|
||||
/**
|
||||
* 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新
|
||||
*/
|
||||
private Date maxUpdateTime;
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void loadErrorCodes() {
|
||||
this.loadErrorCodes0();
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
|
||||
public void refreshErrorCodes() {
|
||||
this.loadErrorCodes0();
|
||||
}
|
||||
|
||||
private void loadErrorCodes0() {
|
||||
// 加载错误码
|
||||
List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeService.getErrorCodeList(applicationName, maxUpdateTime);
|
||||
if (CollUtil.isEmpty(errorCodeRespDTOs)) {
|
||||
return;
|
||||
}
|
||||
log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
|
||||
|
||||
// 刷新错误码的缓存
|
||||
errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
|
||||
// 写入到错误码的缓存
|
||||
putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
|
||||
// 记录下更新时间,方便增量更新
|
||||
maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.errorcode.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeAutoGenerateReqDTO;
|
||||
import cn.iocoder.dashboard.framework.errorcode.core.dto.ErrorCodeRespDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 错误码 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ErrorCodeFrameworkService {
|
||||
|
||||
/**
|
||||
* 自动创建错误码
|
||||
*
|
||||
* @param autoGenerateDTOs 错误码信息
|
||||
*/
|
||||
void autoGenerateErrorCodes(@Valid List<ErrorCodeAutoGenerateReqDTO> autoGenerateDTOs);
|
||||
|
||||
/**
|
||||
* 增量获得错误码数组
|
||||
*
|
||||
* 如果 minUpdateTime 为空时,则获取所有错误码
|
||||
*
|
||||
* @param applicationName 应用名
|
||||
* @param minUpdateTime 最小更新时间
|
||||
* @return 错误码数组
|
||||
*/
|
||||
List<ErrorCodeRespDTO> getErrorCodeList(String applicationName, Date minUpdateTime);
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
/**
|
||||
* 错误码组件
|
||||
*
|
||||
* 将错误码缓存在内存中,同时通过定时器每 n 分钟更新
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.errorcode;
|
@ -1,19 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.excel.core.annotations;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 字典格式化
|
||||
*
|
||||
* 实现将字典数据的值,格式化成字典数据的标签
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface DictFormat {
|
||||
|
||||
SysDictTypeEnum value();
|
||||
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.excel.core.convert;
|
||||
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.iocoder.dashboard.framework.dict.core.util.DictUtils;
|
||||
import cn.iocoder.dashboard.framework.excel.core.annotations.DictFormat;
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.dict.SysDictDataDO;
|
||||
import cn.iocoder.dashboard.modules.system.enums.dict.SysDictTypeEnum;
|
||||
import com.alibaba.excel.converters.Converter;
|
||||
import com.alibaba.excel.enums.CellDataTypeEnum;
|
||||
import com.alibaba.excel.metadata.CellData;
|
||||
import com.alibaba.excel.metadata.GlobalConfiguration;
|
||||
import com.alibaba.excel.metadata.property.ExcelContentProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Excel {@link SysDictDataDO} 数据字典转换器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class DictConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 使用字典解析
|
||||
SysDictTypeEnum type = getType(contentProperty);
|
||||
String label = cellData.getStringValue();
|
||||
SysDictDataDO dictData = DictUtils.parseDictDataFromCache(type.getValue(), label);
|
||||
if (dictData == null) {
|
||||
log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label);
|
||||
return null;
|
||||
}
|
||||
// 将 String 的 value 转换成对应的属性
|
||||
Class<?> fieldClazz = contentProperty.getField().getType();
|
||||
return Convert.convert(fieldClazz, dictData.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 空时,返回空
|
||||
if (object == null) {
|
||||
return new CellData<>("");
|
||||
}
|
||||
|
||||
// 使用字典格式化
|
||||
SysDictTypeEnum type = getType(contentProperty);
|
||||
String value = String.valueOf(object);
|
||||
SysDictDataDO dictData = DictUtils.getDictDataFromCache(type.getValue(), value);
|
||||
if (dictData == null) {
|
||||
log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value);
|
||||
return new CellData<>("");
|
||||
}
|
||||
// 生成 Excel 小表格
|
||||
return new CellData<>(dictData.getLabel());
|
||||
}
|
||||
|
||||
private static SysDictTypeEnum getType(ExcelContentProperty contentProperty) {
|
||||
return contentProperty.getField().getAnnotation(DictFormat.class).value();
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.excel.core.convert;
|
||||
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import com.alibaba.excel.converters.Converter;
|
||||
import com.alibaba.excel.enums.CellDataTypeEnum;
|
||||
import com.alibaba.excel.metadata.CellData;
|
||||
import com.alibaba.excel.metadata.GlobalConfiguration;
|
||||
import com.alibaba.excel.metadata.property.ExcelContentProperty;
|
||||
|
||||
/**
|
||||
* Excel Json 转换器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class JsonConvert implements Converter<Object> {
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
throw new UnsupportedOperationException("暂不支持,也不需要");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CellData<String> convertToExcelData(Object value, ExcelContentProperty contentProperty,
|
||||
GlobalConfiguration globalConfiguration) {
|
||||
// 生成 Excel 小表格
|
||||
return new CellData<>(JsonUtils.toJsonString(value));
|
||||
}
|
||||
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.excel.core.util;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Excel 工具类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ExcelUtils {
|
||||
|
||||
/**
|
||||
* 将列表以 Excel 响应给前端
|
||||
*
|
||||
* @param response 响应
|
||||
* @param filename 文件名
|
||||
* @param sheetName Excel sheet 名
|
||||
* @param head Excel head 头
|
||||
* @param data 数据列表哦
|
||||
* @param <T> 泛型,保证 head 和 data 类型的一致性
|
||||
* @throws IOException 写入失败的情况
|
||||
*/
|
||||
public static <T> void write(HttpServletResponse response, String filename, String sheetName,
|
||||
Class<T> head, List<T> data) throws IOException {
|
||||
// 输出 Excel
|
||||
EasyExcel.write(response.getOutputStream(), head)
|
||||
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
|
||||
.sheet(sheetName).doWrite(data);
|
||||
// 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
|
||||
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
|
||||
}
|
||||
|
||||
public static <T> List<T> raed(MultipartFile file, Class<T> head) throws IOException {
|
||||
return EasyExcel.read(file.getInputStream(), head, null)
|
||||
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
|
||||
.doReadAllSync();
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 基于 EasyExcel 实现 Excel 相关的操作
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.excel;
|
@ -1,12 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.file.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(FileProperties.class)
|
||||
public class FileConfiguration {
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.file.config;
|
||||
|
||||
import cn.iocoder.dashboard.modules.infra.controller.file.InfFileController;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@ConfigurationProperties(prefix = "yudao.file")
|
||||
@Validated
|
||||
@Data
|
||||
public class FileProperties {
|
||||
|
||||
/**
|
||||
* 对应 {@link InfFileController#}
|
||||
*/
|
||||
@NotNull(message = "基础文件路径不能为空")
|
||||
private String basePath;
|
||||
|
||||
// TODO 七牛、等等
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 文件的存储,推荐使用七牛、阿里云、华为云、腾讯云等文件服务
|
||||
*
|
||||
* 在不采用云服务的情况下,我们有几种技术选型:
|
||||
* 方案 1. 使用自建的文件服务,例如说 minIO、FastDFS 等等
|
||||
* 方案 2. 使用服务器的文件系统存储
|
||||
* 方案 3. 使用数据库进行存储
|
||||
*
|
||||
* 如果考虑额外在搭建服务,推荐方案 1。
|
||||
* 对于方案 2 来说,如果要实现文件存储的高可用,需要多台服务器之间做实时同步,可以基于 rsync + inotify 来做
|
||||
* 对于方案 3 的话,实现起来最简单,但是数据库本身不适合存储海量的文件
|
||||
*
|
||||
* 综合考虑,暂时使用方案 3 的方式,比较适合这样一个 all in one 的项目。
|
||||
* 随着文件的量级大了之后,还是推荐采用云服务。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.file;
|
@ -1,42 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigureAfter(RedisAutoConfiguration.class)
|
||||
public class IdempotentConfiguration {
|
||||
|
||||
@Bean
|
||||
public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
|
||||
return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
|
||||
return new IdempotentRedisDAO(stringRedisTemplate);
|
||||
}
|
||||
|
||||
// ========== 各种 IdempotentKeyResolver Bean ==========
|
||||
|
||||
@Bean
|
||||
public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
|
||||
return new DefaultIdempotentKeyResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
|
||||
return new ExpressionIdempotentKeyResolver();
|
||||
}
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.annotation;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 幂等注解
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Idempotent {
|
||||
|
||||
/**
|
||||
* 幂等的超时时间,默认为 1 秒
|
||||
*
|
||||
* 注意,如果执行时间超过它,请求还是会进来
|
||||
*/
|
||||
int timeout() default 1;
|
||||
/**
|
||||
* 时间单位,默认为 SECONDS 秒
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 提示信息,正在执行中的提示
|
||||
*/
|
||||
String message() default "重复请求,请稍后重试";
|
||||
|
||||
/**
|
||||
* 使用的 Key 解析器
|
||||
*/
|
||||
Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
|
||||
/**
|
||||
* 使用的 Key 参数
|
||||
*/
|
||||
String keyArg() default "";
|
||||
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.aop;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
|
||||
import cn.iocoder.dashboard.util.collection.CollectionUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class IdempotentAspect {
|
||||
|
||||
/**
|
||||
* IdempotentKeyResolver 集合
|
||||
*/
|
||||
private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
|
||||
|
||||
private final IdempotentRedisDAO idempotentRedisDAO;
|
||||
|
||||
public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
|
||||
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
|
||||
this.idempotentRedisDAO = idempotentRedisDAO;
|
||||
}
|
||||
|
||||
@Before("@annotation(idempotent)")
|
||||
public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
// 获得 IdempotentKeyResolver
|
||||
IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
|
||||
Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
|
||||
// 解析 Key
|
||||
String key = keyResolver.resolver(joinPoint, idempotent);
|
||||
|
||||
// 锁定 Key。
|
||||
boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
|
||||
// 锁定失败,抛出异常
|
||||
if (!success) {
|
||||
log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
|
||||
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
|
||||
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
|
||||
/**
|
||||
* 幂等 Key 解析器接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface IdempotentKeyResolver {
|
||||
|
||||
/**
|
||||
* 解析一个 Key
|
||||
*
|
||||
* @param idempotent 幂等注解
|
||||
* @param joinPoint AOP 切面
|
||||
* @return Key
|
||||
*/
|
||||
String resolver(JoinPoint joinPoint, Idempotent idempotent);
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
|
||||
/**
|
||||
* 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
|
||||
*
|
||||
* 为了避免 Key 过长,使用 MD5 进行“压缩”
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
|
||||
|
||||
@Override
|
||||
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
String methodName = joinPoint.getSignature().toString();
|
||||
String argsStr = StrUtil.join(",", joinPoint.getArgs());
|
||||
return SecureUtil.md5(methodName + argsStr);
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
|
||||
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 基于 Spring EL 表达式,
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
|
||||
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
|
||||
@Override
|
||||
public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
// 获得被拦截方法参数名列表
|
||||
Method method = getMethod(joinPoint);
|
||||
Object[] args = joinPoint.getArgs();
|
||||
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
|
||||
// 准备 Spring EL 表达式解析的上下文
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
||||
if (ArrayUtil.isNotEmpty(parameterNames)) {
|
||||
for (int i = 0; i < parameterNames.length; i++) {
|
||||
evaluationContext.setVariable(parameterNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
Expression expression = expressionParser.parseExpression(idempotent.keyArg());
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
}
|
||||
|
||||
private static Method getMethod(JoinPoint point) {
|
||||
// 处理,声明在类上的情况
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
if (!method.getDeclaringClass().isInterface()) {
|
||||
return method;
|
||||
}
|
||||
|
||||
// 处理,声明在接口上的情况
|
||||
try {
|
||||
return point.getTarget().getClass().getDeclaredMethod(
|
||||
point.getSignature().getName(), method.getParameterTypes());
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.idempotent.core.redis;
|
||||
|
||||
import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
|
||||
|
||||
/**
|
||||
* 幂等 Redis DAO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class IdempotentRedisDAO {
|
||||
|
||||
private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
|
||||
"idempotent:%s", // 参数为 uuid
|
||||
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
|
||||
String redisKey = formatKey(key);
|
||||
return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
|
||||
}
|
||||
|
||||
private static String formatKey(String key) {
|
||||
return String.format(IDEMPOTENT.getKeyTemplate(), key);
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现
|
||||
* 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。
|
||||
*
|
||||
* 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。
|
||||
*
|
||||
* 和 it4alla/idempotent 组件的差异点,主要体现在两点:
|
||||
* 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力
|
||||
* 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。
|
||||
* 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.idempotent;
|
@ -1,35 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.jackson.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.jackson.deser.LocalDateTimeDeserializer;
|
||||
import cn.iocoder.dashboard.framework.jackson.ser.LocalDateTimeSerializer;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
public JsonUtils jsonUtils(ObjectMapper objectMapper) {
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
/*
|
||||
* 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型
|
||||
* 2. 新增LocalDateTime序列化、反序列化规则
|
||||
*/
|
||||
simpleModule
|
||||
// .addSerializer(Long.class, ToStringSerializer.instance)
|
||||
// .addSerializer(Long.TYPE, ToStringSerializer.instance)
|
||||
.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE)
|
||||
.addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
|
||||
|
||||
objectMapper.registerModules(simpleModule);
|
||||
|
||||
JsonUtils.init(objectMapper);
|
||||
return new JsonUtils();
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.jackson.deser;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime反序列化规则
|
||||
* <p>
|
||||
* 会将毫秒级时间戳反序列化为LocalDateTime
|
||||
*/
|
||||
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer();
|
||||
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.jackson.ser;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* LocalDateTime序列化规则
|
||||
* <p>
|
||||
* 会将LocalDateTime序列化为毫秒级时间戳
|
||||
*/
|
||||
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||
|
||||
public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer();
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.config;
|
||||
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.iocoder.dashboard.framework.lock4j.core.DefaultLockFailureStrategy;
|
||||
import cn.iocoder.dashboard.framework.lock4j.core.Lock4jRedisKeyConstants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class Lock4jConfiguration {
|
||||
|
||||
static {
|
||||
// 手动加载 Lock4jRedisKeyConstants 类,因为它不会被使用到
|
||||
// 如果不加载,会导致 Redis 监控,看到它的 Redis Key 枚举
|
||||
ClassUtil.loadClass(Lock4jRedisKeyConstants.class.getName());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultLockFailureStrategy lockFailureStrategy() {
|
||||
return new DefaultLockFailureStrategy();
|
||||
}
|
||||
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.core;
|
||||
|
||||
import cn.iocoder.dashboard.common.exception.ServiceException;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import com.baomidou.lock.LockFailureStrategy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 自定义获取锁失败策略,抛出 {@link cn.iocoder.dashboard.common.exception.ServiceException} 异常
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultLockFailureStrategy implements LockFailureStrategy {
|
||||
|
||||
@Override
|
||||
public void onLockFailure(String key, long acquireTimeout, int acquireCount) {
|
||||
log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取超时时长:{} ms]", Thread.currentThread().getName(), key, acquireTimeout);
|
||||
throw new ServiceException(GlobalErrorCodeConstants.LOCKED);
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.lock4j.core;
|
||||
|
||||
import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine;
|
||||
import org.redisson.api.RLock;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
|
||||
|
||||
/**
|
||||
* Lock4j Redis Key 枚举类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface Lock4jRedisKeyConstants {
|
||||
|
||||
RedisKeyDefine LOCK4J = new RedisKeyDefine("分布式锁",
|
||||
"lock4j:%s", // 参数来自 DefaultLockKeyBuilder 类
|
||||
HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.lock4j;
|
@ -1,34 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.filter.ApiAccessLogFilter;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.web.config.WebProperties;
|
||||
import cn.iocoder.dashboard.framework.web.core.enums.FilterOrderEnum;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
|
||||
@Configuration
|
||||
public class ApiLogConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 ApiAccessLogFilter Bean,记录 API 请求日志
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<ApiAccessLogFilter> apiAccessLogFilter(WebProperties webProperties,
|
||||
@Value("${spring.application.name}") String applicationName,
|
||||
ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService);
|
||||
return createFilterBean(filter, FilterOrderEnum.API_ACCESS_LOG_FILTER);
|
||||
}
|
||||
|
||||
private static <T extends Filter> FilterRegistrationBean<T> createFilterBean(T filter, Integer order) {
|
||||
FilterRegistrationBean<T> bean = new FilterRegistrationBean<>(filter);
|
||||
bean.setOrder(order);
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.filter;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.ApiAccessLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
|
||||
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
|
||||
import cn.iocoder.dashboard.framework.web.config.WebProperties;
|
||||
import cn.iocoder.dashboard.framework.web.core.util.WebFrameworkUtils;
|
||||
import cn.iocoder.dashboard.util.date.DateUtils;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API 访问日志 Filter
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends OncePerRequestFilter {
|
||||
|
||||
private final WebProperties webProperties;
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !request.getRequestURI().startsWith(webProperties.getApiPrefix());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 获得开始时间
|
||||
Date beginTim = new Date();
|
||||
// 提前获得参数,避免 XssFilter 过滤处理
|
||||
Map<String, String> queryString = ServletUtil.getParamMap(request);
|
||||
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtil.getBody(request) : null;
|
||||
|
||||
try {
|
||||
// 继续过滤器
|
||||
filterChain.doFilter(request, response);
|
||||
// 正常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, null);
|
||||
} catch (Exception ex) {
|
||||
// 异常执行,记录日志
|
||||
createApiAccessLog(request, beginTim, queryString, requestBody, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void createApiAccessLog(HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
ApiAccessLogCreateDTO accessLog = new ApiAccessLogCreateDTO();
|
||||
try {
|
||||
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
|
||||
apiAccessLogFrameworkService.createApiAccessLogAsync(accessLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), JsonUtils.toJsonString(accessLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildApiAccessLogDTO(ApiAccessLogCreateDTO accessLog, HttpServletRequest request, Date beginTime,
|
||||
Map<String, String> queryString, String requestBody, Exception ex) {
|
||||
// 处理用户信息
|
||||
accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
|
||||
accessLog.setUserType(WebFrameworkUtils.getUserType(request));
|
||||
// 设置访问结果
|
||||
CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
|
||||
if (result != null) {
|
||||
accessLog.setResultCode(result.getCode());
|
||||
accessLog.setResultMsg(result.getMsg());
|
||||
} else if (ex != null) {
|
||||
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
|
||||
accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
} else {
|
||||
accessLog.setResultCode(0);
|
||||
accessLog.setResultMsg("");
|
||||
}
|
||||
// 设置其它字段
|
||||
accessLog.setTraceId(TracerUtils.getTraceId());
|
||||
accessLog.setApplicationName(applicationName);
|
||||
accessLog.setRequestUrl(request.getRequestURI());
|
||||
Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
|
||||
accessLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
accessLog.setRequestMethod(request.getMethod());
|
||||
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
accessLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
// 持续时间
|
||||
accessLog.setBeginTime(beginTime);
|
||||
accessLog.setEndTime(new Date());
|
||||
accessLog.setDuration((int) DateUtils.diff(accessLog.getEndTime(), accessLog.getBeginTime()));
|
||||
}
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiAccessLogCreateDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* API 访问日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiAccessLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 访问日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @return 是否创建成功
|
||||
*/
|
||||
Future<Boolean> createApiAccessLogAsync(@Valid ApiAccessLogCreateDTO createDTO);
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.apilog.core.service.dto.ApiErrorLogCreateDTO;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* API 错误日志 Framework Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ApiErrorLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 创建 API 错误日志
|
||||
*
|
||||
* @param createDTO 创建信息
|
||||
* @return 是否创建成功
|
||||
*/
|
||||
Future<Boolean> createApiErrorLogAsync(@Valid ApiErrorLogCreateDTO createDTO);
|
||||
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 访问日志创建 DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class ApiAccessLogCreateDTO {
|
||||
|
||||
/**
|
||||
* 链路追踪编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 用户编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 开始请求时间
|
||||
*/
|
||||
@NotNull(message = "开始请求时间不能为空")
|
||||
private Date beginTime;
|
||||
/**
|
||||
* 结束请求时间
|
||||
*/
|
||||
@NotNull(message = "结束请求时间不能为空")
|
||||
private Date endTime;
|
||||
/**
|
||||
* 执行时长,单位:毫秒
|
||||
*/
|
||||
@NotNull(message = "执行时长不能为空")
|
||||
private Integer duration;
|
||||
/**
|
||||
* 结果码
|
||||
*/
|
||||
@NotNull(message = "错误码不能为空")
|
||||
private Integer resultCode;
|
||||
/**
|
||||
* 结果提示
|
||||
*/
|
||||
private String resultMsg;
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.apilog.core.service.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* API 错误日志创建 DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class ApiErrorLogCreateDTO implements Serializable {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
@NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
@NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
@NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
@NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
@NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
@NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 异常时间
|
||||
*/
|
||||
@NotNull(message = "异常时间不能为空")
|
||||
private Date exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*/
|
||||
@NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*/
|
||||
@NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*/
|
||||
@NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*/
|
||||
@NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
/**
|
||||
* 异常的栈轨迹异常的栈轨迹
|
||||
*/
|
||||
@NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*/
|
||||
@NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OperateLogConfiguration {
|
||||
|
||||
@Bean
|
||||
public OperateLogAspect operateLogAspect() {
|
||||
return new OperateLogAspect();
|
||||
}
|
||||
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core.annotations;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 操作日志注解
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface OperateLog {
|
||||
|
||||
// ========== 模块字段 ==========
|
||||
|
||||
/**
|
||||
* 操作模块
|
||||
*
|
||||
* 为空时,会尝试读取 {@link Api#value()} 属性
|
||||
*/
|
||||
String module() default "";
|
||||
/**
|
||||
* 操作名
|
||||
*
|
||||
* 为空时,会尝试读取 {@link ApiOperation#value()} 属性
|
||||
*/
|
||||
String name() default "";
|
||||
/**
|
||||
* 操作分类
|
||||
*
|
||||
* 实际并不是数组,因为枚举不能设置 null 作为默认值
|
||||
*/
|
||||
OperateTypeEnum[] type() default {};
|
||||
|
||||
// ========== 开关字段 ==========
|
||||
|
||||
/**
|
||||
* 是否记录操作日志
|
||||
*/
|
||||
boolean enable() default true;
|
||||
/**
|
||||
* 是否记录方法参数
|
||||
*/
|
||||
boolean logArgs() default true;
|
||||
/**
|
||||
* 是否记录方法结果的数据
|
||||
*/
|
||||
boolean logResultData() default true;
|
||||
|
||||
}
|
@ -1,354 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core.aop;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum;
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.service.OperateLogFrameworkService;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
|
||||
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import cn.iocoder.dashboard.util.servlet.ServletUtils;
|
||||
import com.google.common.collect.Maps;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
|
||||
import static cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
|
||||
|
||||
/**
|
||||
* 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。
|
||||
* 满足如下任一条件,则会进行记录:
|
||||
* 1. 使用 @ApiOperation + 非 @GetMapping
|
||||
* 2. 使用 @OperateLog 注解
|
||||
*
|
||||
* 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class OperateLogAspect {
|
||||
|
||||
/**
|
||||
* 用于记录操作内容的上下文
|
||||
*
|
||||
* @see SysOperateLogCreateReqVO#getContent()
|
||||
*/
|
||||
private static final ThreadLocal<String> CONTENT = new ThreadLocal<>();
|
||||
/**
|
||||
* 用于记录拓展字段的上下文
|
||||
*
|
||||
* @see SysOperateLogCreateReqVO#getExts()
|
||||
*/
|
||||
private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();
|
||||
|
||||
@Resource
|
||||
private OperateLogFrameworkService operateLogFrameworkService;
|
||||
|
||||
@Around("@annotation(apiOperation)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable {
|
||||
// 可能也添加了 @ApiOperation 注解
|
||||
OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class);
|
||||
return around0(joinPoint, operateLog, apiOperation);
|
||||
}
|
||||
|
||||
@Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)") // 兼容处理,只添加 @OperateLog 注解的情况
|
||||
public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable {
|
||||
return around0(joinPoint, operateLog, null);
|
||||
}
|
||||
|
||||
private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable {
|
||||
// 记录开始时间
|
||||
Date startTime = new Date();
|
||||
try {
|
||||
// 执行原有方法
|
||||
Object result = joinPoint.proceed();
|
||||
// 记录正常执行时的操作日志
|
||||
this.log(joinPoint, operateLog, apiOperation, startTime, result, null);
|
||||
return result;
|
||||
} catch (Throwable exception) {
|
||||
this.log(joinPoint, operateLog, apiOperation, startTime, null, exception);
|
||||
throw exception;
|
||||
} finally {
|
||||
clearThreadLocal();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setContent(String content) {
|
||||
CONTENT.set(content);
|
||||
}
|
||||
|
||||
public static void addExt(String key, Object value) {
|
||||
if (EXTS.get() == null) {
|
||||
EXTS.set(new HashMap<>());
|
||||
}
|
||||
EXTS.get().put(key, value);
|
||||
}
|
||||
|
||||
private static void clearThreadLocal() {
|
||||
CONTENT.remove();
|
||||
EXTS.remove();
|
||||
}
|
||||
|
||||
private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
try {
|
||||
// 判断不记录的情况
|
||||
if (!isLogEnable(joinPoint, operateLog)) {
|
||||
return;
|
||||
}
|
||||
// 真正记录操作日志
|
||||
this.log0(joinPoint, operateLog, apiOperation, startTime, result, exception);
|
||||
} catch (Throwable ex) {
|
||||
log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]",
|
||||
joinPoint, operateLog, apiOperation, result, exception, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
SysOperateLogCreateReqVO operateLogVO = new SysOperateLogCreateReqVO();
|
||||
// 补全通用字段
|
||||
operateLogVO.setTraceId(TracerUtils.getTraceId());
|
||||
operateLogVO.setStartTime(startTime);
|
||||
// 补充用户信息
|
||||
fillUserFields(operateLogVO);
|
||||
// 补全模块信息
|
||||
fillModuleFields(operateLogVO, joinPoint, operateLog, apiOperation);
|
||||
// 补全请求信息
|
||||
fillRequestFields(operateLogVO);
|
||||
// 补全方法信息
|
||||
fillMethodFields(operateLogVO, joinPoint, operateLog, startTime, result, exception);
|
||||
|
||||
// 异步记录日志
|
||||
operateLogFrameworkService.createOperateLogAsync(operateLogVO);
|
||||
}
|
||||
|
||||
private static void fillUserFields(SysOperateLogCreateReqVO operateLogVO) {
|
||||
operateLogVO.setUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
}
|
||||
|
||||
private static void fillModuleFields(SysOperateLogCreateReqVO operateLogVO,
|
||||
ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) {
|
||||
// module 属性
|
||||
if (operateLog != null) {
|
||||
operateLogVO.setModule(operateLog.module());
|
||||
}
|
||||
if (StrUtil.isEmpty(operateLogVO.getModule())) {
|
||||
Api api = getClassAnnotation(joinPoint, Api.class);
|
||||
if (api != null) {
|
||||
// 优先读取 @API 的 name 属性
|
||||
if (StrUtil.isNotEmpty(api.value())) {
|
||||
operateLogVO.setModule(api.value());
|
||||
}
|
||||
// 没有的话,读取 @API 的 tags 属性
|
||||
if (StrUtil.isEmpty(operateLogVO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) {
|
||||
operateLogVO.setModule(api.tags()[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// name 属性
|
||||
if (operateLog != null) {
|
||||
operateLogVO.setName(operateLog.name());
|
||||
}
|
||||
if (StrUtil.isEmpty(operateLogVO.getName()) && apiOperation != null) {
|
||||
operateLogVO.setName(apiOperation.value());
|
||||
}
|
||||
// type 属性
|
||||
if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) {
|
||||
operateLogVO.setType(operateLog.type()[0].getType());
|
||||
}
|
||||
if (operateLogVO.getType() == null) {
|
||||
RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint));
|
||||
OperateTypeEnum operateLogType = convertOperateLogType(requestMethod);
|
||||
operateLogVO.setType(operateLogType != null ? operateLogType.getType() : null);
|
||||
}
|
||||
// content 和 exts 属性
|
||||
operateLogVO.setContent(CONTENT.get());
|
||||
operateLogVO.setExts(EXTS.get());
|
||||
}
|
||||
|
||||
private static void fillRequestFields(SysOperateLogCreateReqVO operateLogVO) {
|
||||
// 获得 Request 对象
|
||||
HttpServletRequest request = ServletUtils.getRequest();
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
// 补全请求信息
|
||||
operateLogVO.setRequestMethod(request.getMethod());
|
||||
operateLogVO.setRequestUrl(request.getRequestURI());
|
||||
operateLogVO.setUserIp(ServletUtil.getClientIP(request));
|
||||
operateLogVO.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
}
|
||||
|
||||
private static void fillMethodFields(SysOperateLogCreateReqVO operateLogVO,
|
||||
ProceedingJoinPoint joinPoint, OperateLog operateLog,
|
||||
Date startTime, Object result, Throwable exception) {
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
operateLogVO.setJavaMethod(methodSignature.toString());
|
||||
if (operateLog == null || operateLog.logArgs()) {
|
||||
operateLogVO.setJavaMethodArgs(obtainMethodArgs(joinPoint));
|
||||
}
|
||||
if (operateLog == null || operateLog.logResultData()) {
|
||||
operateLogVO.setResultData(obtainResultData(result));
|
||||
}
|
||||
operateLogVO.setDuration((int) (System.currentTimeMillis() - startTime.getTime()));
|
||||
// (正常)处理 resultCode 和 resultMsg 字段
|
||||
if (result != null) {
|
||||
if (result instanceof CommonResult) {
|
||||
CommonResult<?> commonResult = (CommonResult<?>) result;
|
||||
operateLogVO.setResultCode(commonResult.getCode());
|
||||
operateLogVO.setResultMsg(commonResult.getMsg());
|
||||
} else {
|
||||
operateLogVO.setResultCode(SUCCESS.getCode());
|
||||
}
|
||||
}
|
||||
// (异常)处理 resultCode 和 resultMsg 字段
|
||||
if (exception != null) {
|
||||
operateLogVO.setResultCode(INTERNAL_SERVER_ERROR.getCode());
|
||||
operateLogVO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) {
|
||||
// 有 @OperateLog 注解的情况下
|
||||
if (operateLog != null) {
|
||||
return operateLog.enable();
|
||||
}
|
||||
// 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况
|
||||
return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null;
|
||||
}
|
||||
|
||||
private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) {
|
||||
if (ArrayUtil.isEmpty(requestMethods)) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(requestMethods).filter(requestMethod ->
|
||||
requestMethod == RequestMethod.POST
|
||||
|| requestMethod == RequestMethod.PUT
|
||||
|| requestMethod == RequestMethod.DELETE)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) {
|
||||
if (ArrayUtil.isEmpty(requestMethods)) {
|
||||
return null;
|
||||
}
|
||||
// 优先,匹配最优的 POST、PUT、DELETE
|
||||
RequestMethod result = obtainFirstLogRequestMethod(requestMethods);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
// 然后,匹配次优的 GET
|
||||
result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET)
|
||||
.findFirst().orElse(null);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
// 兜底,获得第一个
|
||||
return requestMethods[0];
|
||||
}
|
||||
|
||||
private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) {
|
||||
if (requestMethod == null) {
|
||||
return null;
|
||||
}
|
||||
switch (requestMethod) {
|
||||
case GET:
|
||||
return OperateTypeEnum.GET;
|
||||
case POST:
|
||||
return OperateTypeEnum.CREATE;
|
||||
case PUT:
|
||||
return OperateTypeEnum.UPDATE;
|
||||
case DELETE:
|
||||
return OperateTypeEnum.DELETE;
|
||||
default:
|
||||
return OperateTypeEnum.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) {
|
||||
RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解
|
||||
((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class);
|
||||
return requestMapping != null ? requestMapping.method() : new RequestMethod[]{};
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
|
||||
return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
|
||||
return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
|
||||
}
|
||||
|
||||
private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
|
||||
// TODO 提升:参数脱敏和忽略
|
||||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
|
||||
String[] argNames = methodSignature.getParameterNames();
|
||||
Object[] argValues = joinPoint.getArgs();
|
||||
// 拼接参数
|
||||
Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
|
||||
for (int i = 0; i < argNames.length; i++) {
|
||||
String argName = argNames[i];
|
||||
Object argValue = argValues[i];
|
||||
// 被忽略时,标记为 ignore 字符串,避免和 null 混在一起
|
||||
args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
|
||||
}
|
||||
return JsonUtils.toJsonString(args);
|
||||
}
|
||||
|
||||
private static String obtainResultData(Object result) {
|
||||
// TODO 提升:结果脱敏和忽略
|
||||
if (result instanceof CommonResult) {
|
||||
result = ((CommonResult<?>) result).getData();
|
||||
}
|
||||
return JsonUtils.toJsonString(result);
|
||||
}
|
||||
|
||||
private static boolean isIgnoreArgs(Object object) {
|
||||
Class<?> clazz = object.getClass();
|
||||
// 处理数组的情况
|
||||
if (clazz.isArray()) {
|
||||
return IntStream.range(0, Array.getLength(object))
|
||||
.anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
|
||||
}
|
||||
// 递归,处理数组、Collection、Map 的情况
|
||||
if (Collection.class.isAssignableFrom(clazz)) {
|
||||
return ((Collection<?>) object).stream()
|
||||
.anyMatch((Predicate<Object>) o -> isIgnoreArgs(object));
|
||||
}
|
||||
if (Map.class.isAssignableFrom(clazz)) {
|
||||
return isIgnoreArgs(((Map<?, ?>) object).values());
|
||||
}
|
||||
// obj
|
||||
return object instanceof MultipartFile
|
||||
|| object instanceof HttpServletRequest
|
||||
|| object instanceof HttpServletResponse;
|
||||
}
|
||||
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core.enums;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作日志的操作类型
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum OperateTypeEnum {
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*
|
||||
* 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。
|
||||
* 在有需要的时候,通过声明 {@link OperateLog} 注解来记录
|
||||
*/
|
||||
GET(1),
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
CREATE(2),
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
UPDATE(3),
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
DELETE(4),
|
||||
/**
|
||||
* 导出
|
||||
*/
|
||||
EXPORT(5),
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
IMPORT(6),
|
||||
/**
|
||||
* 其它
|
||||
*
|
||||
* 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识
|
||||
*/
|
||||
OTHER(0);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer type;
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core;
|
@ -1,17 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core.service;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.controller.logger.vo.operatelog.SysOperateLogCreateReqVO;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public interface OperateLogFrameworkService {
|
||||
|
||||
/**
|
||||
* 异步记录操作日志
|
||||
*
|
||||
* @param reqVO 操作日志请求
|
||||
* @return true: 记录成功,false: 记录失败
|
||||
*/
|
||||
Future<Boolean> createOperateLogAsync(SysOperateLogCreateReqVO reqVO);
|
||||
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.logger.operatelog.core.util;
|
||||
|
||||
import cn.iocoder.dashboard.framework.logger.operatelog.core.aop.OperateLogAspect;
|
||||
|
||||
/**
|
||||
* 操作日志工具类
|
||||
* 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class OperateLogUtils {
|
||||
|
||||
public static void setContent(String content) {
|
||||
OperateLogAspect.setContent(content);
|
||||
}
|
||||
|
||||
public static void addExt(String key, Object value) {
|
||||
OperateLogAspect.addExt(key, value);
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* 日志组件,包括:
|
||||
*
|
||||
* 1. 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。
|
||||
* 2. API 日志:包含两类
|
||||
* 2.1 API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。
|
||||
* 2.2 API 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。
|
||||
* 3. 通用 Logger 日志:将 {@link org.slf4j.Logger} 打印的日志,只满足大于等于配置的 {@link org.slf4j.event.Level} 进行持久化,可以理解成简易的“日志中心”。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.logger;
|
@ -1,9 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.monitor.config;
|
||||
|
||||
import de.codecentric.boot.admin.server.config.EnableAdminServer;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableAdminServer
|
||||
public class AdminServerConfiguration {
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 使用 Spring Boot Admin 实现简单的监控平台
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.monitor;
|
@ -1 +0,0 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/Admin/?yudao>
|
@ -1,34 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBaits 配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@MapperScan(value = "${yudao.info.base-package}", annotationClass = Mapper.class,
|
||||
lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
|
||||
public class MybatisConfiguration {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
|
||||
return mybatisPlusInterceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MetaObjectHandler defaultMetaObjectHandler(){
|
||||
return new DefaultDBFieldHandler(); // 自动填充参数类
|
||||
}
|
||||
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.dataobject;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 基础实体对象
|
||||
*/
|
||||
@Data
|
||||
public class BaseDO implements Serializable {
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 创建者,目前使用 SysUser 的 id 编号
|
||||
*
|
||||
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private String creator;
|
||||
/**
|
||||
* 更新者,目前使用 SysUser 的 id 编号
|
||||
*
|
||||
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private String updater;
|
||||
/**
|
||||
* 是否删除
|
||||
*/
|
||||
@TableLogic
|
||||
private Boolean deleted;
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.handler;
|
||||
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.dashboard.framework.security.core.LoginUser;
|
||||
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 通用参数填充实现类
|
||||
*
|
||||
* 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
|
||||
*
|
||||
* @author hexiaowu
|
||||
*/
|
||||
public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
|
||||
Date current = new Date();
|
||||
|
||||
// 创建时间为空,则以当前时间为插入时间
|
||||
if (Objects.isNull(baseDO.getCreateTime())) {
|
||||
baseDO.setCreateTime(current);
|
||||
}
|
||||
// 更新时间为空,则以当前时间为更新时间
|
||||
if (Objects.isNull(baseDO.getUpdateTime())) {
|
||||
baseDO.setUpdateTime(current);
|
||||
}
|
||||
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getCreator())) {
|
||||
baseDO.setCreator(loginUser.getId().toString());
|
||||
}
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(baseDO.getUpdater())) {
|
||||
baseDO.setUpdater(loginUser.getId().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
Object modifyTime = getFieldValByName("updateTime", metaObject);
|
||||
Object modifier = getFieldValByName("updater", metaObject);
|
||||
// 获取登录用户信息
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
|
||||
// 更新时间为空,则以当前时间为更新时间
|
||||
if (Objects.isNull(modifyTime)) {
|
||||
setFieldValByName("updateTime", new Date(), metaObject);
|
||||
}
|
||||
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人
|
||||
if (Objects.nonNull(loginUser) && Objects.isNull(modifier)) {
|
||||
setFieldValByName("updater", loginUser.getId().toString(), metaObject);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.mapper;
|
||||
|
||||
import cn.iocoder.dashboard.common.pojo.PageParam;
|
||||
import cn.iocoder.dashboard.common.pojo.PageResult;
|
||||
import cn.iocoder.dashboard.framework.mybatis.core.util.MyBatisUtils;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力
|
||||
*/
|
||||
public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
|
||||
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
|
||||
// MyBatis Plus 查询
|
||||
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam);
|
||||
selectPage(mpPage, queryWrapper);
|
||||
// 转换返回
|
||||
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
|
||||
}
|
||||
|
||||
default T selectOne(String field, Object value) {
|
||||
return selectOne(new QueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default Integer selectCount(String field, Object value) {
|
||||
return selectCount(new QueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default List<T> selectList() {
|
||||
return selectList(new QueryWrapper<>());
|
||||
}
|
||||
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.query;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能:
|
||||
*
|
||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
*/
|
||||
public class QueryWrapperX<T> extends QueryWrapper<T> {
|
||||
|
||||
public QueryWrapperX<T> likeIfPresent(String column, String val) {
|
||||
if (StringUtils.hasText(val)) {
|
||||
return (QueryWrapperX<T>) super.like(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
|
||||
if (!CollectionUtils.isEmpty(values)) {
|
||||
return (QueryWrapperX<T>) super.in(column, values);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> inIfPresent(String column, Object... values) {
|
||||
if (!ArrayUtils.isEmpty(values)) {
|
||||
return (QueryWrapperX<T>) super.in(column, values);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> eqIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.eq(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> neIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.ne(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> gtIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.gt(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> geIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.ge(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> ltIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.lt(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> leIfPresent(String column, Object val) {
|
||||
if (val != null) {
|
||||
return (QueryWrapperX<T>) super.le(column, val);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public QueryWrapperX<T> betweenIfPresent(String column, Object val1, Object val2) {
|
||||
if (val1 != null && val2 != null) {
|
||||
return (QueryWrapperX<T>) super.between(column, val1, val2);
|
||||
}
|
||||
if (val1 != null) {
|
||||
return (QueryWrapperX<T>) ge(column, val1);
|
||||
}
|
||||
if (val2 != null) {
|
||||
return (QueryWrapperX<T>) le(column, val2);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// ========== 重写父类方法,方便链式调用 ==========
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> eq(boolean condition, String column, Object val) {
|
||||
super.eq(condition, column, val);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> eq(String column, Object val) {
|
||||
super.eq(column, val);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> orderByDesc(String column) {
|
||||
super.orderByDesc(true, column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> last(String lastSql) {
|
||||
super.last(lastSql);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryWrapperX<T> in(String column, Collection<?> coll) {
|
||||
super.in(column, coll);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.type;
|
||||
|
||||
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
|
||||
import cn.iocoder.dashboard.util.json.JsonUtils;
|
||||
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现
|
||||
* 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。
|
||||
*
|
||||
* 例如说哦,{@link SysUserDO#getPostIds()} 属性
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler<Object> {
|
||||
|
||||
private static final TypeReference<Set<Long>> typeReference = new TypeReference<Set<Long>>(){};
|
||||
|
||||
@Override
|
||||
protected Object parse(String json) {
|
||||
return JsonUtils.parseObject(json, typeReference);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(Object obj) {
|
||||
return JsonUtils.toJsonString(obj);
|
||||
}
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package cn.iocoder.dashboard.framework.mybatis.core.util;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.iocoder.dashboard.common.pojo.PageParam;
|
||||
import cn.iocoder.dashboard.common.pojo.SortingField;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MyBatis 工具类
|
||||
*/
|
||||
public class MyBatisUtils {
|
||||
|
||||
public static <T> Page<T> buildPage(PageParam pageParam) {
|
||||
return buildPage(pageParam, null);
|
||||
}
|
||||
|
||||
public static <T> Page<T> buildPage(PageParam pageParam, Collection<SortingField> sortingFields) {
|
||||
// 页码 + 数量
|
||||
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
|
||||
// 排序字段
|
||||
if (!CollectionUtil.isEmpty(sortingFields)) {
|
||||
page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ?
|
||||
OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* 使用 MyBatis Plus 提升使用 MyBatis 的开发效率
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework.mybatis;
|
@ -1 +0,0 @@
|
||||
<http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao>
|
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
|
||||
* 1. core 包:是该组件的核心分装
|
||||
* 2. config 包:是该组件基于 Spring 的配置
|
||||
*
|
||||
* 技术组件,也分成两类:
|
||||
* 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展
|
||||
* 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。
|
||||
*/
|
||||
package cn.iocoder.dashboard.framework;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user