一、常量的定义

以下是阿里编码规约


二、图片的 base64 编码

概述博客

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

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

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

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

博客

springboot 项目总配置 资源路径映射

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

五、事务管理

新建的Spring Boot项目中,一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbcspring-boot-starter-data-jpa的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入DataSourceTransactionManagerJpaTransactionManager。 所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

@Transactional注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务。

例如用户新增需要插入用户表、用户与岗位关联表、用户与角色关联表,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作, 这样可以防止出现脏数据,就可以使用事务让它实现回退。
做法非常简单,我们只需要在方法或类添加@Transactional注解即可。

@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;
}

Transactional注解的常用属性表:

属性 说明
propagation 事务的传播行为,默认值为 REQUIRED。
isolation 事务的隔离度,默认值采用 DEFAULT
timeout 事务的超时时间,默认值为-1,不超时。如果设置了超时时间(单位秒),那么如果超过该时间限制了但事务还没有完成,则自动回滚事务。
read-only 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。{xxx1.class, xxx2.class,……}
noRollbackFor 抛出 no-rollback-for 指定的异常类型,不回滚事务。{xxx1.class, xxx2.class,……}

事务的传播机制是指如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。 即:在执行一个@Transactinal注解标注的方法时,开启了事务;当该方法还在执行中时,另一个人也触发了该方法;那么此时怎么算事务呢,这时就可以通过事务的传播机制来指定处理方式。

TransactionDefinition传播行为的常量:

常量 含义
TransactionDefinition.PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
TransactionDefinition.PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQ

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

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

实现逻辑:先创建一个任务调度线程池(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.loadUserByUsernameauthentication = 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();// 生成tokenreturn 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 */@Overrideprotected 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(){@Overridepublic 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(){@Overridepublic 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 {// 创建一个任务 TimerTaskTimerTask task = new TimerTask() {@Overridepublic 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*/@Overrideprotected 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 字段的长度为 2000operLog.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 的 一些函数

前台页面

SQL语句

concat 函数:

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;

在以上代码中,作者有一处很灵巧的地方。

因为我们用户的角色可能有多个,所有拼接出来sql语句全部用 Or 来进行连接,最后将凭借的sql语句,将第一个 Or 替换为 And ,后面的 Or 连接符依旧使用

# 举个栗子:
-   or d.dpet=1 or d.dept = 2 or d.dept = 3      ===修改后===> AND (d.dept=1 or d.dept=2 or d.dept=3)

sql语句的拼接

十、动态数据源

动态数据源的实现

十一、定时任务

字母哥的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){// 生成唯一的tokenString 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语句

带条件的查询

增加

修改:修改局部数据可以传入对象,而不是写修改单个数据的代码和sql,可以直接写一个一通百通的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
{@Overridepublic 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);// 如果该方法被 @RepeatSubmitif (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;@Autowiredprivate RedisCache redisCache;/*** 间隔时间,单位:秒 默认10秒* * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据*/private int intervalTime = 10;public void setIntervalTime(int intervalTime){this.intervalTime = intervalTime;}@SuppressWarnings("unchecked")@Overridepublic 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的区别

requestMothed Content-type request方法 是否可以获取参数
get getQueryString true
get getInputStream false
get getParameterMap true
post application/x-www-form-urlencoded getQueryString false
post application/x-www-form-urlencoded getInputStream true
post application/x-www-form-urlencoded getParameterMap true
post text/html getQueryString false
post text/html getInputStream false
post text/html getParameterMap true

3. Request 获取参数 1 getParameterMap( ) 2 getInputstream()区别

这两个都是后台获取前端传输的参数:

但是这两个是有区别的:

当请求头: Content-Type: application/x-www-form-urlencoded; charset=UTF-8 (默认情况下) 通过:getparameterMap() 方法获取参数。

当请求头: Content-Type: application/json;charset=UTF-8 或者是: Content-Type: multipart/form-data 则要通过:**getInputstream()**方式获取

RuoYi-Vue 分离版 收获与总结相关推荐

  1. node.js安装模式 的区别_安装 若依 前后端 分离版

    近期安装若依前后端分离 不同于以往的 半分离的jeefast 首先 不同于半分离的最大区别本人感觉安装的步骤比较繁琐 安装的插件比较多 第一步 http://ruoyi.vip/ 官网下载 解压安装 ...

  2. 【若依VUE前后端分离版框架-初探】尚医疗项目1.0

    视频学习来源:[尚学堂]JAVA微服务_医疗管理项目_基于若依快速开发框架:医疗系统(尚医疗)_前后端分离开发_RuoYi开源快速开发平台_spring_网站管理后台_哔哩哔哩_bilibili[课程 ...

  3. 使用SpringBoot + Vue (若依前后端分离版) 写项目的一些总结(持续更新...)

    使用SpringBoot + Vue(若依前后端分离版) 写项目的一些总结 获取Redis服务 @Autowired private RedisCache redisCache; String cap ...

  4. 从零搭建RuoYi若依(非分离版)

    从零搭建RuoYi若依(非分离版) 一.环境搭建 1.准备工作 2.下载源码 3.导入到IDEA 4.修改Maven配置 5.创建数据库 6.修改数据库连接配置 7.启动项目 二.示例:使用ruoyi ...

  5. 若依前后端分离版手把手教你本地搭建环境并运行项目

    场景 RuoYi-Vue是一款基于SpringBoot+Vue的前后端分离极速后台开发框架. RuoYi 官网地址:http://ruoyi.vip RuoYi 在线文档:http://doc.ruo ...

  6. 若依前后端分离版怎样修改主页面显示请求的SpringBoot后台数据

    场景 使用若依的前后端分离版,本来的首页效果是 现在如果要根据具体业务实现从后台获取要显示的数据实现类似下面的效果 注: 博客: https://blog.csdn.net/badao_liumang ...

  7. 若依前后端分离版怎样去掉登录验证码

    场景 若依前后端分离版手把手教你本地搭建环境并运行项目: 若依前后端分离版手把手教你本地搭建环境并运行项目_BADAO_LIUMANG_QIZHI的博客-CSDN博客_若依前后端分离版本的配置 上面在 ...

  8. 若依前后端分离版怎样根据数据库生成代码并快速实现某业务的增删改查

    场景 使用若依的前后端分离版,怎样使用其代码生成实现对单表的增删改查导出的业务. 注: 博客: https://blog.csdn.net/badao_liumang_qizhi 关注公众号 霸道的程 ...

  9. 若依前后端分离版怎样修改主页面和浏览器上的图标和标题

    场景 使用若依的前后端分离版,,其默认的图标和标题等如下 如果想要修改为自己想要的标题和图标,实现类似下面的效果 注: 博客: https://blog.csdn.net/badao_liumang_ ...

最新文章

  1. ftp connect: No route to host 解决过程
  2. python与excel的差别-python数据分析相对于bi和excel的优势是什么?
  3. codeforces 665B Shopping
  4. SAP批次级别的意义及启用操作
  5. 210317阶段三opencv
  6. servlet和filter的区别
  7. 《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一一2.5 为跨年度的个人参赛选手构造记录...
  8. Sightseeing Cows(POJ-3621)
  9. java加载jdbc驱动,加载JDBC驱动
  10. 只存活了9天,谷歌AI伦理委员会解散了:严重低估员工的战斗力
  11. 用python编写决策树算法_详细介绍python实现决策树C4.5算法
  12. 计算机绘图 电子教案,机械制图与计算机绘图电子教案大全.doc
  13. FL studio 20简易入门教程 -- 第八篇 -- 技巧合集
  14. Excel 点击单元格打钩,再点击取消
  15. SPSS 检验后显著性识别
  16. Tomcat里 appBase和docBase的区别
  17. 你必须利用备份恢复数据库,但是你没有控制文件,该如何解决问题呢?
  18. 官方精简版Windows10:微软自己都看不过去了
  19. QQ坦白说之解密教程
  20. Hyper-V虚拟化

热门文章

  1. iOS图案解锁(九宫格)
  2. 用计算机弹大白菜鸡毛菜,抖音大白菜鸡毛菜什么意思 大白菜鸡毛菜意思出处介绍...
  3. golang 下载图片
  4. qq 4 android,Android QQ HD v5.5.4 正式版发布
  5. ERJ | 马来西亚三城室内环境微生物/代谢产物与初中生哮喘的关联
  6. python元祖格式_python中元祖
  7. mysql求学号的总分_有一个student表,有学号,姓名,科目,成绩等字段,请写一条sql语句,算出学生的总分数?...
  8. 太赞了!Github上都没有的“网约车”项目!!!
  9. echart湖南地图
  10. iphone游戏-钢铁侠2 Iron Man评测