RuoYi-Vue 分离版 收获与总结

发布时间 2023-04-22 19:42:10作者: binbinx

https://blog.csdn.net/qq_41965731/article/details/115241184

一、常量的定义

以下是阿里编码规约

 

 

 

二、图片的 base64 编码

https://blog.csdn.net/duola8789/article/details/78844431

概述博客

三、在项目启动时将一些数据提交加载到缓存中

1.利用@PostConstruct注解,当类被初始化时执行 init 方法,将数据库中的数据提前加载到缓存中,避免第一次访问的用户等待时间过长。

/**
* 项目启动时,初始化参数到缓存
*/
@PostConstruct
public void init()
{
// 在数据库中查出配置信息集合
List<SysConfig> configsList = configMapper.selectConfigList(new SysConfig());
for (SysConfig config : configsList)
{
// 将配置参数放入到Redis中
redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
}
}

四、SpringMVC中资源路径映射本地文件

博客

 

@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry)
{
/** 本地文件上传路径 */
registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**") // 访问资源路径
.addResourceLocations("file:" + RuoYiConfig.getProfile() + "/"); // 映射到的资源路径
}
}

五、事务管理

 

@Transactional
public int insertUser(User user)
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}

常见坑点1:遇到检查异常时,事务开启,也无法回滚。 例如下面这段代码,用户依旧增加成功,并没有因为后面遇到检查异常而回滚!!

@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new SQLException("发生异常了..");
}
return rows;
}

原因分析:因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对检查异常进行事务回滚,可以在@Transactional注解里使用 rollbackFor属性明确指定异常。
例如下面这样,就可以正常回滚:

@Transactional(rollbackFor = Exception.class)
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new SQLException("发生异常了..");
}
return rows;
}

常见坑点2: 在业务层捕捉异常后,发现事务不生效。 这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。
例如:下面这段代码直接导致用户新增的事务回滚没有生效。

@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
try
{
// 谨慎:尽量不要在业务层捕捉异常并处理
throw new SQLException("发生异常了..");
}
catch (Exception e)
{
e.printStackTrace();
}
}
return rows;
}

推荐做法:在业务层统一抛出异常,然后在控制层统一处理。

@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new RuntimeException("发生异常了..");
}
return rows;
}

 

六、异步延迟任务、记录登录日志

若依在用户登录后,不管成功或者失败,都会异步延迟将日志记录到数据库中

实现逻辑:
先创建一个任务调度线程池(ScheduledExecutorService),在需要打印日志出创建一个 TimerTask(可以执行一次或通过计时器重复执行的任务),交由任务调度线程池分配线程在等待指定的时间后执行,执行完根据任务调度线程池重写父类 ThreadPoolExecutor afterExecute 方法在任务执行完进行相应的操作

登录的业务层

/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null)
{
// i: 验证码过期,执行异步任务 => 添加到日志数据库中
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new CustomException(e.getMessage());
}
}



// 异步延迟执将日志保存到数据库中
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));



LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}

异步任务管理器

/**
* 异步任务管理器
*
* @author ruoyi
*/
public class AsyncManager
{
/**
* 操作延迟10毫秒
*/
private final int OPERATE_DELAY_TIME = 10;

/**
* 异步操作任务调度线程池
*/
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

/**
* 单例模式
*/
private AsyncManager(){}

private static AsyncManager me = new AsyncManager();

public static AsyncManager me()
{
return me;
}

/**
* 执行任务
*
* @param task 任务
*/
public void execute(TimerTask task)
{
// 创建并执行一次操作,该操作在给定的延迟后变为启用状态。
executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}

/**
* 停止任务线程池
*/
public void shutdown()
{
Threads.shutdownAndAwaitTermination(executor);
}
}

ScheduleExecutorService Bean

/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService()
{
return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
{
// 在构造 ScheduledThreadPoolExecutor 对象的同时重写了父类 ThreadPoolExecutor afterExecute 方法

/*
afterExecute =>
给定Runnable执行完成时调用的方法。 该方法由执行任务的线程调用。 如果不为null,则Throwable是导致执行突然终止的未捕获的 RuntimeException或Error
*/
@Override
protected void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
}

Threads 类

/**
* 线程相关工具类.
*
* @author ruoyi
*/
public class Threads
{
private static final Logger logger = LoggerFactory.getLogger(Threads.class);

/**
* sleep等待,单位为毫秒
*/
public static void sleep(long milliseconds)
{
try
{
Thread.sleep(milliseconds);
}
catch (InterruptedException e)
{
return;
}
}

/**
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* * 如果仍然超時,則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
public static void shutdownAndAwaitTermination(ExecutorService pool)
{
if (pool != null && !pool.isShutdown())
{
pool.shutdown();
try
{
// 如果操作超过 120s 则强制关闭线程池
/*
awaitTermination():
阻塞直到关闭请求后所有任务完成执行,或者发生超时,或者当前线程被中断(以先发生者为准)。
如果该执行程序终止,则为true如果终止之前已超时,则为false
*/
if (!pool.awaitTermination(120, TimeUnit.SECONDS))
{
pool.shutdownNow();
if (!pool.awaitTermination(120, TimeUnit.SECONDS))
{
logger.info("Pool did not terminate");
}
}
}
catch (InterruptedException ie)
{
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

/**
* 打印线程异常信息
*/
public static void printException(Runnable r, Throwable t)
{
if (t == null && r instanceof Future<?>)
{
try
{
Future<?> future = (Future<?>) r;
if (future.isDone())
{
future.get();
}
}
catch (CancellationException ce)
{
t = ce;
}
catch (ExecutionException ee)
{
t = ee.getCause();
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
}
}
if (t != null)
{
logger.error(t.getMessage(), t);
}
}
}

AsynFactory 异步工厂

/**
* 异步工厂(产生任务用)
*
* @author ruoyi
*/
public class AsyncFactory
{
private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");

/**
* 记录登录信息
*
* @param username 用户名
* @param status 状态
* @param message 消息
* @param args 列表
* @return 任务task
*/
public static TimerTask recordLogininfor(final String username, final String status, final String message,
final Object... args)
{
// 获取userAgent信息并进行解析
final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
// 获取登录的ip地址
final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
return new TimerTask()
{
@Override
public void run()
{
// 根据ip获取地址
String address = AddressUtils.getRealAddressByIP(ip);
StringBuilder s = new StringBuilder();
s.append(LogUtils.getBlock(ip));
s.append(address);
s.append(LogUtils.getBlock(username));
s.append(LogUtils.getBlock(status));
s.append(LogUtils.getBlock(message));
// 打印信息到日志
sys_user_logger.info(s.toString(), args);
// 获取客户端操作系统
String os = userAgent.getOperatingSystem().getName();
// 获取客户端浏览器
String browser = userAgent.getBrowser().getName();
// 封装对象
SysLogininfor logininfor = new SysLogininfor();
logininfor.setUserName(username);
logininfor.setIpaddr(ip);
logininfor.setLoginLocation(address);
logininfor.setBrowser(browser);
logininfor.setOs(os);
logininfor.setMsg(message);
// 日志状态
if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
{
logininfor.setStatus(Constants.SUCCESS);
}
else if (Constants.LOGIN_FAIL.equals(status))
{
logininfor.setStatus(Constants.FAIL);
}
// 插入数据
SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
}
};
}

/**
* 操作日志记录
*
* @param operLog 操作日志信息
* @return 任务task
*/
public static TimerTask recordOper(final SysOperLog operLog)
{
return new TimerTask()
{
@Override
public void run()
{
// 远程查询操作地点
operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
}
};
}
}

 

TimerTask实现了 Runnable 接口

自己实现的小Demo

/**
* @Description : 测试实现 异步延迟执行任务
* @Date: 2021/3/25 19:03
* @Author : tiankun
*/
public class MyTest {
public static void main(String[] args) throws IOException {
// 创建一个任务 TimerTask
TimerTask task = new TimerTask() {

@Override
public void run() {
System.out.println("当前线程的名称:"+Thread.currentThread().getName());
System.out.println("阿尼哈赛有!!!");
}
};

// 创建任务调度线程池 ScheduledExecutorService
// 参数一:核心线程数(保留在池中的线程数(即使它们处于空闲状态),除非设置了allowCoreThreadTimeOut)
// 参数二:创建线程的工厂
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(
3,
// namingPattern 设置新的BasicThreadFactory使用的命名模式
// daemon 为新的BasicThreadFactory设置守护程序标志
new BasicThreadFactory.Builder().namingPattern("tk-%d").daemon(true).build()
)
{
/*
重写 ThreadPoolExecute 的 afterExecute 方法
afterExecute => 给定Runnable执行完成时调用的方法。 该方法由执行任务的线程调用。 如果不为null,则Throwable是导致执行突然终止的未捕获的 RuntimeException或Error
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("任务执行完该做的事情");
}
};

/*
由任务调度线程池分配线程执行任务

参数一:要执行的任务
参数二:从现在开始延迟执行的时间
参数三:延迟参数的时间单位
*/
scheduledExecutorService.schedule(task,5, TimeUnit.SECONDS);
for (int i = 1; i <= 5; i++) {
try {
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}


// 使的程序执行,避免JVM关闭
System.in.read();
}
}

输出打印结果

1
2
3
4
当前线程的名称:tk-1
阿尼哈赛有!!!
任务执行完该做的事情
5

七、自定义注解+AOP => 实现系统日志保存

Log 注解

/**
* 自定义操作日志记录注解
*
* @author ruoyi
*
*/
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
/**
* 模块
*/
public String title() default "";

/**
* 功能
*/
public BusinessType businessType() default BusinessType.OTHER;

/**
* 操作人类别
*/
public OperatorType operatorType() default OperatorType.MANAGE;

/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
}

业务操作类型 BusinessType 枚举类

/**
* 业务操作类型
*
* @author ruoyi
*/
public enum BusinessType
{
/**
* 其它
*/
OTHER,

/**
* 新增
*/
INSERT,

/**
* 修改
*/
UPDATE,

/**
* 删除
*/
DELETE,

/**
* 授权
*/
GRANT,

/**
* 导出
*/
EXPORT,

/**
* 导入
*/
IMPORT,

/**
* 强退
*/
FORCE,

/**
* 生成代码
*/
GENCODE,

/**
* 清空数据
*/
CLEAN,
}

 操作人类别 OperatorType 枚举类

/**
* 操作人类别
*
* @author ruoyi
*/
public enum OperatorType
{
/**
* 其它
*/
OTHER,

/**
* 后台用户
*/
MANAGE,

/**
* 手机端用户
*/
MOBILE
}

注解的实例示例

/**
* 修改用户
*/
@PreAuthorize("@ss.hasPermi('system:user:edit')")
@Log(title = "用户管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysUser user)
{
userService.checkUserAllowed(user);
if (StringUtils.isNotEmpty(user.getPhonenumber())
&& UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user)))
{
return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
else if (StringUtils.isNotEmpty(user.getEmail())
&& UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user)))
{
return AjaxResult.error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
}
user.setUpdateBy(SecurityUtils.getUsername());
return toAjax(userService.updateUser(user));
}

AOP 配置

/**
* 操作日志记录处理
*
* @author ruoyi
*/
@Aspect
@Component
public class LogAspect
{
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

// 配置织入点 / 切入点
@Pointcut("@annotation(com.ruoyi.common.annotation.Log)")
public void logPointCut()
{
}

/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
{
handleLog(joinPoint, null, jsonResult);
}

/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e)
{
handleLog(joinPoint, e, null);
}

protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
{
try
{
// 获得注解
Log controllerLog = getAnnotationLog(joinPoint);
if (controllerLog == null)
{
return;
}

// 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());

// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
operLog.setOperIp(ip);
// 返回参数
operLog.setJsonResult(JSON.toJSONString(jsonResult));

operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
if (loginUser != null)
{
operLog.setOperName(loginUser.getUsername());
}

if (e != null)
{
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
// 设置方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog);
// 保存数据库
AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
}
catch (Exception exp)
{
// 记录本地异常日志
log.error("==前置通知异常==");
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}

/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
{
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
operLog.setTitle(log.title());
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request,参数和值
if (log.isSaveRequestData())
{
// 获取参数的信息,传入到数据库中。
setRequestValue(joinPoint, operLog);
}
}

/**
* 获取请求的参数,放到log中
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
{
String requestMethod = operLog.getRequestMethod();
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
{
String params = argsArrayToString(joinPoint.getArgs());
// 数据库中的 OperParam 字段的长度为 2000
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
}
else
{
Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
}
}

/**
* 是否存在注解,如果存在就获取
*/
private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
{
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();

if (method != null)
{
return method.getAnnotation(Log.class);
}
return null;
}

/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray)
{
String params = "";
if (paramsArray != null && paramsArray.length > 0)
{
for (int i = 0; i < paramsArray.length; i++)
{
if (!isFilterObject(paramsArray[i]))
{
Object jsonObj = JSON.toJSON(paramsArray[i]);
params += jsonObj.toString() + " ";
}
}
}
return params.trim();
}

/**
* 判断是否需要过滤的对象 。
* 需要过滤的对象 MultipartFile
* HttpServletRequest
* HttpServletResponse
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o)
{
Class<?> clazz = o.getClass();
if (clazz.isArray())
{
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
}
else if (Collection.class.isAssignableFrom(clazz))
{
Collection collection = (Collection) o;
for (Iterator iter = collection.iterator(); iter.hasNext();)
{
return iter.next() instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz))
{
Map map = (Map) o;
for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
{
Map.Entry entry = (Map.Entry) iter.next();
return entry.getValue() instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
}
}

 

八、Mysql 的 一些函数

前台页面

 

 

 data_format 函数的使用

FIND_IN_SET函数的概述,以及与IN LIKE 的区别

常用 mysql 函数

九、数据权限

在实际开发中,需要设置用户只能查看哪些部门的数据,这种情况一般称为数据权限。
例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。

若依实现数据权限的步骤

# 0.实现数据权限的普遍为查询操作,判断你是过滤掉你没有权限看到的数据
# 1.在需要被数据权限管理的表中加入一个 dataScope 字段,用来表示该用户所拥有的数据权限(这个字段可以给用户加,也可以给角色加,若依是给角色加)
# 2.自定义一个注解,用来标识在需要被数据权限限制的方法上,以便于我们通过AOP来对数据进行相对应的操作 (若依自定义了一个DataScope的注解,里面字段代码sql语句数据库所对应的表名,我个人认为可以只定义一个注解,只起到标识的作用,至于表的别名,可以提取到配置文件中,当程序进行启动时,通过配置文件解析出来数据封装到一个常量类中,如果需要使用,就可以直接操作常量类即可)
# 3.编写一个切面,对标有 数据权限的注解 进行拦截,根据数据权限类型的不同进行sql的生成,并且加入查询数据的属性中(若依在设计时,所有的实体类有一个父类,baseEntity,里面定义了一些实体类共有的属性,如:创建人,创建时间..等,其中就是 param 属性,可以用来放置一些我们自定义的参数,我们可以将生成的sql封装进对象中)
# 4.在我们查询的sql语句的最后面 加上取出我们存储的sql语句 ${param.filter_data_sql} ,如果有则进行数据权限的过滤,如果没有则不实现

DataScope 注解

/**
* 数据权限过滤注解
*
* @author ruoyi
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";

/**
* 用户表的别名
*/
public String userAlias() default "";
}

DataScope注解定义在业务层,因为这样可以获取到完整的参数数据

@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user)
{
return userMapper.selectUserList(user);
}

DataScope的AOP => 过滤sql的生成

/**
* 数据过滤处理
*
* @author ruoyi
*/
@Aspect
@Component
public class DataScopeAspect
{
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";

/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";

/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";

/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";

/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";

/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";

// 配置织入点
@Pointcut("@annotation(com.ruoyi.common.annotation.DataScope)")
public void dataScopePointCut()
{
}

// 前置通知
@Before("dataScopePointCut()")
public void doBefore(JoinPoint point) throws Throwable
{
handleDataScope(point);
}

protected void handleDataScope(final JoinPoint joinPoint)
{
// 获得注解
DataScope controllerDataScope = getAnnotationLog(joinPoint);
if (controllerDataScope == null)
{
return;
}
// 获取当前的用户
LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNotNull(loginUser))
{
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
{
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
}
}
}

/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param userAlias 别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
{
StringBuilder sqlString = new StringBuilder();

for (SysRole role : user.getRoles())
{
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope))
{
sqlString = new StringBuilder();
break;
}
else if (DATA_SCOPE_CUSTOM.equals(dataScope))
{
// 根据 角色部门表 自己被分配的权限
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
}
else if (DATA_SCOPE_DEPT.equals(dataScope))
{
// 拥有该角色对应部门的权限
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
}
else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
{
// 拥有该角色对应部门以及其子部门的权限
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
}
else if (DATA_SCOPE_SELF.equals(dataScope))
{
if (StringUtils.isNotBlank(userAlias))
{
// 只有本人的数据权限
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
}
else
{
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 ");
}
}
}

if (StringUtils.isNotBlank(sqlString.toString()))
{
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
{
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}

/**
* 是否存在注解,如果存在就获取
*/
private DataScope getAnnotationLog(JoinPoint joinPoint)
{
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();

if (method != null)
{
return method.getAnnotation(DataScope.class);
}
return null;
}
}

baseEntity 类

public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;

/** 搜索值 */
private String searchValue;

/** 创建者 */
private String createBy;

/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;

/** 更新者 */
private String updateBy;

/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;

/** 备注 */
private String remark;

/** 请求参数 */
private Map<String, Object> params;

 

十、动态数据源

动态数据源的实现

十一、定时任务

字母哥的springboot 教程里的 quartz 教程

十二、在线用户查询

因为用户登录后会在redis中存储一份登录信息,所以可以通过 redisTemplate.keys(pattern) 匹配出我们定义特点字符串存储用户的key

 

用户的强制退出:

直接删除该用户缓存中的 redis 即可实现用户的强制退出功能

十三、 token的设计

若依在 token 里面设置了很多信息,如果将这些数据生产jwt则会生产很长的字符串,这样在每次请求都会携带这个巨长无比的字符串会占用网络的带宽,增加访问的时长,所以若依将这样登录会的用户信息(LoginUser对象)放入到了redis中,并生成了唯一的字符串作为 key,随后通过这个key去生成 jwt token ,这样将大大降低了 token 的大小,在随后每次访问解析 token 获取key ,在redis中获取 登录的用户信息即可。

/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
// 生成唯一的token
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
// 刷新令牌
refreshToken(loginUser);

Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}


/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser)
{
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}

/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}


/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}

 

十四、可以借鉴的一些SQL语句

带条件的查询

 

 

十五、防止请求的重复提交

大致思路

先定义一个 @RepeatSubmit 注解,如果那块的表现层(Controller)方法需要防止重复提交就将其加上 @RepeatSubmit , 定义一个拦截器,在 preHandle 方法中获取方法,并且获取 @RepeatSubmit 注解,如果不为null 则表示该Controller需要方式请求的重复提交,将当前的请求地址和当前人的一个标识作为key 去获取redis里面的数据,获取数据与这次请求进行对比,如果数据一致则拒接请求向下传递,并给前端返回相对应的提示信息,如果没有redis中获取到数据或者比对不成功,则以当前的请求地址与当前操作用户的标识作为 key 将这次请求的信息放入到 redis中。

@RepeatSubmit 注解

/**
* 自定义注解防止表单重复提交
*
* @author ruoyi
*
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{

}

 RepeatSubmitInteceptor 拦截器

/**
* 防止重复提交拦截器
*
* @author ruoyi
*/
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{

HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 如果该方法被 @RepeatSubmit
if (annotation != null)
{
if (this.isRepeatSubmit(request))
{
AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return super.preHandle(request, response, handler);
}
}

/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request
* @return
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request);
}

--------------------

/**
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
* @author ruoyi
*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
public final String REPEAT_PARAMS = "repeatParams";

public final String REPEAT_TIME = "repeatTime";

// 令牌自定义标识
@Value("${token.header}")
private String header;

@Autowired
private RedisCache redisCache;

/**
* 间隔时间,单位:秒 默认10秒
*
* 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
*/
private int intervalTime = 10;

public void setIntervalTime(int intervalTime)
{
this.intervalTime = intervalTime;
}

@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request)
{
String nowParams = "";
// 先通过 getInputStream 获取数据,如果获取不到说明该请求的参数是get请,通过 Parameter方式获取数据即可。
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}

// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();

// 唯一值(没有消息头则使用请求地址)
String submitKey = request.getHeader(header);
if (StringUtils.isEmpty(submitKey))
{
submitKey = url;
}

// 唯一标识(指定key + 消息头)
String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;

Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
return false;
}

/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}

/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < (this.intervalTime * 1000))
{
return true;
}
return false;
}
}

十六、getParameterMap 和 getInputstream 区别

参考:

https://blog.csdn.net/iteye_9007/article/details/82640063

https://blog.csdn.net/qq_27727251/article/details/79855305

https://blog.csdn.net/cpongo3/article/details/89327612

1. getInputStream()与getParameterMap()获得Post请求的数据区别

1 这是一个HTTP/HTTPS请求

2 请求方法是POST(querystring无论是否POST都将被设置到parameter中)

3 请求的类型(Content-Type头)是application/x-www-form-urlencoded

4 Servlet调用了getParameter系列方法

如果上述条件没有同时满足,则相关的表单数据不会被设置进request的parameter集合中,相关的数据可以通过request.getInputStream()来访问。反之,如果上述条件均满足,相关的表单数据将不能再通过request.getInputStream()来读取。

request.getInputStream() 只能读一次

BufferedReader reader = new BufferedReader(new InputStreamReader(req.getInputStream()));
String body = IOUtils.read(reader);
String name = req.getParameter(“name”);
if(StringUtils.isNotBlank(body)){
log.info(“business receive somthing with body :”+body);

2. java HttpServletRequest的getQueryString,getInputStream,getParameterMap的区别