mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-04 12:18:42 +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