后台管理系统-知识梳理

后台管理系统知识梳理

此处只记录部分知识点 , 具体所有知识梳理内容均在源码以注解形式保存

项目介绍 : https://www.cnblogs.com/Leo-Heng/p/14989398.html

前端源码 : https://gitee.com/leo-heng/myproject-front

后端源码 : https://gitee.com/leo-heng/myproject-back

目录结构

myproject
 ├── pom.xml
 └── src
     └── main
         ├── java
         │   └── com
         │       └── myproject
         │           └── cn
         │               ├── common
         │               │   ├── domin //登录验证
         │               │   │   └── LoginAdmin.java
         │               │   ├── exception //异常处理
         │               │   │   ├── ApiException.java
         │               │   │   ├── FormValidException.java
         │               │   │   └── GlobalExceptionHandler.java
         │               │   ├── http //返回信息
         │               │   │   ├── AxiosResult.java
         │               │   │   └── AxiosStatus.java
         │               │   ├── page //分页查询
         │               │   │   └── PageBean.java
         │               │   ├── perm //权限校验
         │               │   │   ├── HasPerm.java
         │               │   │   └── PermAop.java
         │               │   ├── props //相关属性
         │               │   │   └── UploadProperties.java
         │               │   └── valid //分组校验
         │               │       ├── anno
         │               │       │   ├── InList.java
         │               │       │   └── InListHandler.java
         │               │       └── group
         │               │           ├── AddGroup.java
         │               │           ├── BtnGroup.java
         │               │           ├── DirectoryGrop.java
         │               │           ├── MenuGroup.java
         │               │           └── UpdateGroup.java
         │               ├── components //相关组件
         │               │   ├── CacheService.java //redis缓存
         │               │   ├── DemoScheduling.java //定时任务测试demo
         │               │   ├── EmailService.java //email发送
         │               │   └── TokenService.java //token生成解析
         │               ├── config //相关配置
         │               │   ├── CorsConfig.java //跨域请求配置
         │               │   ├── MyWebConfig.java //登录拦截
         │               │   └── SwaggerConfig.java //swagger2文档配置
         │               ├── constants //常量
         │               │   └── RedisKey.java //redis分组常量
         │               ├── controller //相关业务
         │               │   ├── AdminController.java
         │               │   ├── base
         │               │   │   └── BaseController.java
         │               │   ├── BrandController.java
         │               │   ├── CommonController.java
         │               │   ├── MenuController.java
         │               │   ├── RoleController.java
         │               │   └── ScheduleController.java
         │               ├── dto //返回前端的封装数据
         │               │   ├── AdminDTO.java
         │               │   ├── base
         │               │   │   └── BaseDTO.java
         │               │   ├── BrandDTO.java
         │               │   ├── MenuDTO.java
         │               │   ├── RoleDTO.java
         │               │   └── ScheduleBeanDTO.java
         │               ├── entity //实体类
         │               │   ├── Admin.java
         │               │   ├── AdminRole.java
         │               │   ├── base
         │               │   │   └── BaseEntity.java
         │               │   ├── Brand.java
         │               │   ├── Menu.java
         │               │   ├── Role.java
         │               │   ├── RoleMenu.java
         │               │   └── ScheduleBean.java
         │               ├── interceptor //登录拦截
         │               │   └── LoginInterceptor.java
         │               ├── mapper //mapper查询
         │               │   ├── AdminMapper.java
         │               │   ├── AdminRoleMapper.java
         │               │   ├── base
         │               │   │   └── MyMapper.java
         │               │   ├── BrandMapper.java
         │               │   ├── MenuMapper.java
         │               │   ├── RoleMapper.java
         │               │   ├── RoleMenuMapper.java
         │               │   └── ScheduleMapper.java
         │               ├── MyApplication.java
         │               ├── query //查询封装
         │               │   ├── AdminQuery.java
         │               │   ├── base
         │               │   │   └── BaseQuery.java
         │               │   ├── BrandQuery.java
         │               │   ├── MenuQuery.java
         │               │   └── RoleQuery.java
         │               ├── service //service层
         │               │   ├── AdminService.java
         │               │   ├── base 
         │               │   │   ├── BaseService.java //所有service接口的父类
         │               │   │   └── impl
         │               │   │       └── BaseServiceImpl.java //所有serviceImpl的父类
         │               │   ├── BrandService.java
         │               │   ├── impl
         │               │   │   ├── AdminServiceImpl.java
         │               │   │   ├── BrandServiceImpl.java
         │               │   │   ├── MenuServiceImpl.java
         │               │   │   ├── RoleServiceImpl.java
         │               │   │   └── ScheduleServiceImpl.java
         │               │   ├── MenuService.java
         │               │   ├── RoleService.java
         │               │   └── ScheduleService.java
         │               ├── transfer //类型转换 entity转DTO
         │               │   ├── AdminTransfer.java
         │               │   ├── base
         │               │   │   └── BaseTransfer.java
         │               │   ├── BrandTransfer.java
         │               │   ├── MenuTransfer.java
         │               │   ├── RoleTransfer.java
         │               │   └── ScheduleBeanTransfer.java
         │               └── utils //工具类
         │                   ├── FormValidUtils.java //表单校验
         │                   ├── JsonUtils.java //json转换
         │                   ├── ReflectionUtils.java //反射工具
         │                   ├── TreeUtils.java //树状数据
         │                   └── UploadService.java //下载配置
         └── resources //配置文件
             ├── application.yml 
             └── templates //freemark模板引擎默认加载文档
                 └── salary.ftl //工资邮件模板

后端知识点

直接对源码进行分析 , 一些没必要分析的类直接跳过

cn / common / exception / GlobalExceptionHandler

spring异常处理

/**
 * @className: GlobalExceptionHandler
 * @description: spring异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    //操作失败异常的异常处理器
    @ExceptionHandler(ApiException.class)
    public AxiosResult<Void> handlerFormValidException(ApiException e) {
        return AxiosResult.error(e.getAxiosStatus());
    }

    //全局异常的异常处理器
    @ExceptionHandler(Throwable.class)
    public AxiosResult<Void> handlerThrowable(Throwable e) {
        System.out.println(e);
        AxiosStatus uploadError = AxiosStatus.UNKNOWN_ERROR;
        uploadError.setMessage(e.getMessage());
        return AxiosResult.error(uploadError);
    }

    @ExceptionHandler(FormValidException.class)
    public AxiosResult<Map<String, String>> handlerFormValidException(FormValidException e) {
        AxiosStatus axiosStatus = e.getAxiosStatus();
        return AxiosResult.error(axiosStatus, e.getMap());
    }

    //表单校验异常的异常处理器 ,
    // 当表单校验失败时会抛出一个携带错误信息的MethodArgumentNotValidException异常
    // 此时使用异常处理器处理此异常向前端返回携带的错误信息
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public AxiosResult<String> handlerFormValidException(MethodArgumentNotValidException e) {
        //获取抛出要异常中存储的异常信息
        BindingResult bindingResult = e.getBindingResult();

        AxiosStatus formValidError = AxiosStatus.FORM_VALID_ERROR;
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        //当携带的内容不为空时 , 返回携带的错误
        if (!CollectionUtils.isEmpty(fieldErrors)) {
            FieldError fieldError = fieldErrors.get(0);
            String defaultMessage = fieldError.getDefaultMessage();
            formValidError.setMessage(defaultMessage);
            return AxiosResult.error(formValidError);
        }

        formValidError.setMessage("表单校验错误");
        return AxiosResult.error(formValidError);
    }
}

cn / common / perm / PermAop

/**
 * @className: PermAop
 * @description: Aop切面 , 用于权限校验 , 如果登录角色没有对应权限则报错
 */
@Aspect //切面声明,标注在类、接口(包括注解类型)或枚举上
@Component
public class PermAop {
    @Autowired
    private TokenService tokenService;

    //@Before 前置通知 , 先于pointcut设置的切入点执行
    @Before("pointCut()")
    public void checkBtnPerm(JoinPoint joinPoint) {
        //获取当前执行的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //获取当前方法是否含有HasPerm注解
        HasPerm annotation = method.getAnnotation(HasPerm.class);
        //不为null说明含有注解
        if (annotation != null) {
            //获取当前注解设置的权限标识
            String s = annotation.perSign();
            System.out.println(s);
            //获取当前登录用户的所有权限
            List<Menu> menus = tokenService.getLoginAdminCache().getMenus();
            //使用anyMatch判断是否含有对应权限 , 如果含有则放行 , 否则抛出异常
            boolean b = menus.stream().anyMatch(menu -> s.equalsIgnoreCase(menu.getPermSign()));
            System.out.println(b);
            if (!b) {
                throw new ApiException(AxiosStatus.NO_PERM);
            }
        }
    }

    //@Pointcut 设置切入点表达式 , 选择controller包下的所有类的所有方法 方法的参数可以是任何东西 , 都需要经过切面
    @Pointcut("execution(* com.myproject.cn.controller.*.*(..))")
    public void pointCut() {

    }
}

cn / components / DemoScheduling

package com.myproject.cn.components;

import org.springframework.scheduling.annotation.Scheduled;

/**
 * @className: DemoScheduling
 * @description: StringTask定时任务相关知识点笔记 , 邮件发送相关笔记 , 
 */
//@Component
public class DemoScheduling {
    /**
     * stringTask的使用方式 :
     * 1. 手写
     * Crontrigger trigger = new CronTrigger(* * * * * *);
     * ThreadPoolTaskScheduler
     * 好处 : 手动指定线程池大小 , 手动初始化
     *
     * 2. 使用Spring封装的注解
     *  1. 开启任务调度 @EnableScheduling
     *  2. 在容器中的某个类 , 定义一个方法 , 在方法中做任务调度做的事情
     *  这个事情什么时候做 , 需要在这个方法上添加一个注解@Scheduled ,
     *  通过这个注解中的配置cron表达式等 , 确定这个方法什么时候执行
     *  好处 : 使用起来简单 , 只需要2个注解
     *  缺点 : 默认的线程池只有一个
     *
     *  解决方案 :
     *  解决方案一 : 调度就使用一个线程 , 但是这个线程所做的事情放到子线程中去做 , 此时这个线程不会阻塞
     *  解决方案二 : 修改调度线程次大小
     */

    /**
     *  发送邮件 :
     *      导包 : spring-context-support-javaxmail
     *  相关配置 :
     *      javaMailSender
     *      from
     *      邮箱授权码
     *      smtp服务器
     *
     *  freemarker模板引擎使用方法 :
     *      1. 导包freemarker
     *      2. 配置freemarkerConfiger
     *              配置模板地址
     *              配置字符编码集
     *      3. 在配置的默认地址文件夹(templates)中 , 定义xxx.ftl格式文件
     *      4. 在这个格式文件中 , 可变的值使用${key}定义
     *      5. 编码读取模板 , 获取字符串 , 发送邮件
     */

    //    @Autowired
//    private AdminMapper adminMapper;

    //    @Autowired
//    private EmailService emailService;

    /**
     * 定时任务 :
     *      定时任务的功能如果是一个耗时操作 , 应该放到线程池中
     *      使用@Scheduled注解创建定时任务 , 会在系统启动后自动执行
     *      @Scheduled 线程池默认只有一个线程 ,
     *      corn表示执行频率或者执行时机
     *      *从前到后依次表示 秒 分 时 日 月 年
     *      0 30 16 15 * * 表示每个月15日 下午4时 30分执行任务
     *      可以通过扩大线程池容量 或 开辟新线程来实现操作
     *      Scheduled注解特点 : 自动触发 , 表达式无法修改
     *      可以动态的从数据库中获取表达式后 , 手动触发 , 如果想做到动态触发需要使用@EnableScheduling
     *
     */
//    @Scheduled(cron = "0 30 16 15 * *")
//    public void doSomeThing() throws Exception {
//        System.out.println(Thread.currentThread().getName());
//        List<KafkaProperties.Admin> admins = adminMapper.selectList(null);
//        admins.forEach(admin -> {
//            //使用异步线程池来做的
//            emailService.sendMail(admin);
//        });
//        //要给这里面的员工发邮件
//
//    }
    //@Scheduled的特点 : 自动触发执行 ,  表达式无法修改
    //解决方案 : 动态从数据库中获取数据后 , 手动触发
    //如果想要做到动态需要使用@EnableScheduling + @Scheduled(cron=)
    /*

    @Scheduled(cron = "0/5 * * * * *")
    public void doSomeThing(){
        System.out.println("xxxxx");
    }

    */
}

cn / components / TokenService

Token的用途 :

  1. 前端登录
  2. 获取前端用户信息

前端登录操作流程 :

  1. 用户登录
  2. 传入后台用户信息的map集合
  3. 用户信息输入正确 , 生成uuid , loginAdmin对象 , 并将uuid作为key , LoginAdmin转为json字符串作为value存入redis
  4. 将uuid封装为token返回前端

**前端获取登录用户信息流程 : **

  1. 前端发送请求获取用户信息
  2. 将token封装入请求头
  3. 从请求中获取携带的token
  4. 解析token携带的uuid
  5. 根据uuid从redis中找出LoginAdmin对应的json字符串并转为对象 , 将对象返回前端

问 : 为什么不把uuid直接返回前端 , 而是将uuid封装入token , 将token返回前端

答 : 使用token是规范 并且token有检测机制 比如过期等 如果直接返回uuid功能可以实现 但是没有过期机制 并且不符合规范

/**
 * 由于前端设置了每次请求都会携带token , 所以可以从请求中头中获取token ,
 * 根据token可以获取loginAdmin对象在redis中对应的uuid
 * loginAdmin对象中页存储了这个个uuid
 *
 * @className: TokenService
 * @description: 将对象转为token
 */
@Component
public class TokenService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //token加密
    private static final String secret = "ljkaidop";

    //将admin封装为一个token
    public String createTokenAndCacheLoginAdmin(Admin admin) {
        //生成随机uuid
        String s = UUID.randomUUID().toString();
        //创建一个loginAdmin对象封装登录信息
        LoginAdmin loginAdmin = new LoginAdmin();
        loginAdmin.setUuid(s);
        loginAdmin.setAdmin(admin);
        //uuid作为key , loginAdmin对象转为json放入redis
        setLoginAdminCache(loginAdmin);
        //将uuid封装入token传回前端
        return createToken(s);

    }

    /**
     * 使用redis缓存登录用户
     *
     * @param loginAdmin
     */
    public void setLoginAdminCache(LoginAdmin loginAdmin) {
        //将loginAdmin的uuid作为key , loginAdmin对象转为json作为value存入redis
        stringRedisTemplate.opsForValue().set(loginAdmin.getUuid(), JsonUtils.obj2Str(loginAdmin));
    }

    /**
     * 生成token
     */
    public String createToken(String uuid) {
        Algorithm algorithm = Algorithm.HMAC256(secret);
        String token = JWT.create()
                .withIssuer("Leo")//设置签发者
                .withSubject("登录token")//设置主题
                .withClaim("uuid", uuid)
                .sign(algorithm);
        return token;
    }

    /**
     * 解析request内的token
     * 解析失败时会抛出异常
     *
     * @param request
     * @return
     */
    public DecodedJWT verifyToken(HttpServletRequest request) {
        String authentication = request.getHeader("Authentication"); //从request中获取token
        String token = authentication.split(" ")[1]; // 去除token的前缀  获取token内容
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("Leo")
                .withSubject("登录token")
                .build();
        DecodedJWT jwt = verifier.verify(token);
        return jwt;
    }

    /**
     * 解析token中的uuid
     *
     * @return
     */
    public String getUUID() {
        DecodedJWT jwt = verifyToken(getRequest());
        String uuid = jwt.getClaim("uuid").asString();
        return uuid;
    }

    /**
     * 从redis缓存中获取loginAdmin的json格式并转为java对象
     */
    public LoginAdmin getLoginAdminCache() {
        String uuId = getUUID();
        String s = stringRedisTemplate.opsForValue().get(uuId);
        LoginAdmin loginAdmin = JsonUtils.str2Obj(s, LoginAdmin.class);
        return loginAdmin;
    }

    public Long adminI() {
        Long id = getLoginAdminCache().getAdmin().getId();
        return id;
    }

    public boolean isAdmin() {
        Boolean isAdmin = getLoginAdminCache().getAdmin().getIsAdmin();
        return isAdmin;
    }

    /**
     * 判断token是否正确 , 用于登录校验
     * @param httpServletRequest
     * @return
     */
    public boolean checkTokenIsSure(HttpServletRequest httpServletRequest) {
        //从请求头中获取token
        String authentication = httpServletRequest.getHeader("Authentication");
        //没有传递token
        if (StringUtils.isEmpty(authentication))
            throw new ApiException(AxiosStatus.NO_LOGIN);

        //token没有携带前缀 , token格式错误
        if (!authentication.startsWith("Bearer "))
            throw new ApiException(AxiosStatus.NO_LOGIN);

        //token式错误
        String[] s = authentication.split(" ");
        if (s.length != 2)
            throw new ApiException(AxiosStatus.NO_LOGIN);

        //如果没有解析出异常 , 则token正确
        try {
            verifyToken(httpServletRequest);
        } catch (Exception e) {
            throw new ApiException(AxiosStatus.NO_LOGIN);
        }
        return true;
    }

    /**
     * 获取全局request
     *
     * @return
     */
    public HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        System.out.println("request = " + request);
        return request;
    }


    /**
     * 获取全局response
     *
     * @return
     */
    public HttpServletResponse getResponse() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes.getResponse();
    }
}

cn / config / CorsConfig

/**
 * @className: CorsConfig
 * @description: 跨域过滤器
 */
@Configuration
public class CorsConfig {

    /**
     * 设置跨域请求相关配置
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许所有访问源地址
        corsConfiguration.addAllowedOrigin("*");
        // 允许所有请求方法
        corsConfiguration.addAllowedMethod("*");
        // 允许所有请求头
        corsConfiguration.addAllowedHeader("*");
        // spring boot2.4版本后configuration.setAllowCredentials(true)的时候对addAllowedOrigin设置为"*",
        // 而要使用 allowedOriginPatterns这个字段来设置origin。(本项目使用2.3.3)
        // corsConfiguration用于是否发送cookie信息
        corsConfiguration.setAllowCredentials(false);

        //设置跨域过滤器的过滤内容
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
        CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
        return corsFilter;

    }
}

cn / controller / CommonController

/**
 * @className: CommonController
 * @description:
 */
@RestController
@RequestMapping("common")
public class CommonController {
    /**
     * 文件上传 :
     * 以前 : ssm整合 , 文件上传需要开启multipart , 如果不限制文件上传大小则可随意上传
     * 现在 : springBoot , 文件上传默认开启multipart , 默认的tomcat上传的文件大小为1M
     * 设置文件上传的限制:
     * 1. 格式限制 jpg,png
     * 2. 最大不超过200kb
     * 3. 宽高为100*100
     * 4. 上传的文件必须是图片
     * <p>
     * ssm中redis如何使用
     * redis
     * 需要配置jedis或者lettuce
     * slf4j
     * 配置RedisTemplate
     * StringRedisTemplate
     * 配置工程 , 数据库连接池
     * <p>
     * 登录拦截 :
     * 登录时查询数据库如果用户存在 , 密码匹配成功则登录
     * 给前端返回一个token , token携带了缓存redis中的用户key
     */
    @Autowired
    private UploadService uploadService;

    @Autowired
    private AdminService adminService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private RoleService roleService;

    @Autowired
    private MenuService menuService;

    @Autowired
    private MenuTransfer menuTransfer;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private TokenService tokenService;


    @PostMapping("upload")
    @ApiOperation(value = "文件上传")
    public AxiosResult<String> upload(@RequestPart Part part) throws IOException {
        return AxiosResult.success(uploadService.upload(part));
    }

    @GetMapping("getCode/{uuid}")
    @ApiOperation(value = "获取验证码")
    public AxiosResult<String> getCode(@PathVariable String uuid) {
        //使用gif验证码 , 设置宽高和字符数
        GifCaptcha gifCaptcha = new GifCaptcha(130, 48, 4);
        //获取验证码的对应文字
        String text = gifCaptcha.text();
        //将前端传回的uuid作为key , 验证码的值作为value存入redis
        stringRedisTemplate.opsForValue().set(RedisKey.CODE_KEY + uuid, text);
        //将验证码图片转为base64
        String s = gifCaptcha.toBase64();
        return AxiosResult.success(s);
    }

    @PostMapping("doLogin")
    @ApiOperation(value = "登录")
    public AxiosResult<String> deLogin(@RequestBody Map<String, String> map) {
        String code = map.get("code");
        String uuid = map.get("uuid");
        String username = map.get("username");
        String password = map.get("password");
        String value = stringRedisTemplate.opsForValue().get(RedisKey.CODE_KEY + uuid);
        //判断验证码是否正确
        if (StringUtils.isEmpty(value) || !value.equalsIgnoreCase(code))
            throw new ApiException(AxiosStatus.ERROR);
        Admin admin = adminService.getAdminByAccount(username);
        //判断用户名是否正确
        if (admin == null)
            throw new ApiException(AxiosStatus.ERROR);
        //判断验证码是否正确
        if (!bCryptPasswordEncoder.matches(password, admin.getAdminPassword()))
            throw new ApiException(AxiosStatus.ERROR);
        //校验成功
        //清除redis中的验证码数据
        Boolean delete = stringRedisTemplate.delete(RedisKey.CODE_KEY + uuid);
        String token = tokenService.createTokenAndCacheLoginAdmin(admin);
        return AxiosResult.success(token);
    }


    @GetMapping("getUserInfo")
    @ApiOperation(value = "获取用户信息")
    public AxiosResult<Map<String, Object>> getUserInfo(HttpServletRequest httpServletRequest) {
        //从redis中获取存储的登录用户
        LoginAdmin loginAdmin = tokenService.getLoginAdminCache();
        System.out.println("登录用户 : " + loginAdmin);
        Admin admin = loginAdmin.getAdmin();
        System.out.println("admin : " + admin);
        Map<String, Object> map = new HashMap<>();

        map.put("admin", admin);
        //如果登录用户是超级管理员则赋予所有权限
        if (admin.getIsActive()) {
            //查询所有权限
            List<Menu> list = menuService.list();
            //将用户所有权限放入loginAdmin中
            loginAdmin.setMenus(list);
            List<MenuDTO> menuDTOS = menuTransfer.toDTO(list);
            //获取除按钮权限外的其他权限
            List<MenuDTO> collect = menuDTOS.stream().filter(menuDTO -> !menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
            //获取所有按钮权限
            List<MenuDTO> btnMenus = menuDTOS.stream().filter(menuDTO -> menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
            //获取所有根级权限
            List<MenuDTO> root = collect.stream().filter(menuDTO -> menuDTO.getParentId().equals(0L)).collect(Collectors.toList());
            //将列表排序并转为树形结构
            TreeUtils.buildTreeData(root, collect);
            map.put("menuList", root);
            map.put("btnList", btnMenus);
            //将loginAdmin存入存入redis
            tokenService.setLoginAdminCache(loginAdmin);
            return AxiosResult.success(map);

        } else {
            //如果登录用户不是超级管理员返回admin的用户权限
            //获取admin对应角色
            List<Long> roleIds = adminService.getRolesByAdminId(admin.getId());
            //获取角色对应权限
            List<MenuDTO> menuDTOs = roleService.getMenusByRoleIds(roleIds);
            List<Menu> menus = menuTransfer.toEntity(menuDTOs);
            //设置用户对应权限
            loginAdmin.setMenus(menus);
            System.out.println("所有权限为 : " + menuDTOs);
            //获取除按钮权限外的其他权限
            List<MenuDTO> collect = menuDTOs.stream().filter(menuDTO -> !menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
            //获取所有按钮权限
            List<MenuDTO> btnMenus = menuDTOs.stream().filter(menuDTO -> menuDTO.getMenuType().equals(3)).collect(Collectors.toList());
            System.out.println("菜单 : " + collect);
            //获取所有根级权限
            List<MenuDTO> root = collect.stream().filter(menuDTO -> menuDTO.getParentId().equals(0L)).collect(Collectors.toList());
            //将列表排序并转为树形结构
            TreeUtils.buildTreeData(root, collect);
            map.put("menuList", root);
            map.put("btnList", btnMenus);
            //将loginAdmin存入存入redis
            tokenService.setLoginAdminCache(loginAdmin);
            return AxiosResult.success(map);
        }
    }

    /*
        以前登录拦截的实现方式 :
        1. 登录 , 登录成功后把用户信息存入session
        2. 定义拦截器 , 从session总获取登录信息
            如果能获取说明登录了
            获取不到说明未登录
        产生的问题 :
        1. 前端端分离时 , 使用axios请求时 , 默认不携带cookie ,
           所以每次访问的session都不同 , 登录拦截失效
        2. 解决方案 : 携带cookie
        3. 实际开发中项目为了做到分流的效果 , 需要部署集群
           1个tomcat的并发量默认值为200 , 可以调整 ,
           最多调整到500左右 , 性能还行 ,如果调整过高 , 性能就会变差
           因此在实际生产中 : 要把项目部署多次 , 启动多个tomcat , 占用不同的端口 , 使用nginx做负载均衡
           此时想要使用session做登录状态 , 就需要共享session
           但是session是有状态的

        因此产生的思想 :
        1. 在登录成功时 , 给前端返回一个字符串 , 前端接收到字符串保存起来
        2. 前端每次请求都需要在请求头中添加这个字符串 , 后台拦截器获取这个字符串
           如果得到字符串说明登录 , 没得到说明没登录

        这个字符串不能是明文 , 不能轻易让人识别出来 因此需要使用token , jwt
        token是一个字符串 , 这个字符串中间有2个"." , 将字符串分为3部分
     */
//    @GetMapping("loginStatus")
//    @ApiOperation(value = "获取登录状态")
//    public String loginStatus(HttpSession httpSession) {
//        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//        System.out.println(requestAttributes.getClass().getName());
//        return "success";
//    }

}

cn / controller / ScheduleController

手动设置定时任务的暂停 重启 删除 添加

/**
 * @className: ScheduleController
 * @description:
 */
@RestController
@RequestMapping("schedule")
@Api(tags = "定时管理", description = "定时管理")
public class ScheduleController extends BaseController {

    @Autowired
    private ScheduleService scheduleService;
    @Autowired
    private ScheduleBeanTransfer scheduleBeanTransfer;
    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    @Autowired
    private AdminMapper adminMapper;
    @Autowired
    private EmailService emailService;

    private Map<Long, ScheduledFuture> map = new HashMap<>();

    //手动执行定时任务
    @PostConstruct//java自带注解 , 被修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次
    public void initSchedule() {
        System.out.println("PostConstruct方法执行");
        List<ScheduleBean> list = scheduleService.list();
        list.forEach(scheduleBean -> {
            //获取cron表达式
            String cornExpress = scheduleBean.getCronExpress();
            //创建CronTrigger
            CronTrigger cronTrigger = new CronTrigger(cornExpress);
            //设置定时任务 , 将任务放到子线程执行
            ScheduledFuture<?> scheduledFuture = threadPoolTaskScheduler.schedule(new Runnable() {
                @Override
                public void run() {
                    //发送邮件
                    List<Admin> admins = adminMapper.selectList(null);
                    admins.forEach(admin -> emailService.sendMail(admin));
                }
            }, cronTrigger);
            //存入map , 方便切换
            map.put(scheduleBean.getCronId(), scheduledFuture);
        });
    }

    @ApiOperation(value = "查询所有cron表达式")
    @GetMapping
    public AxiosResult<List<ScheduleBeanDTO>> list() {
        return AxiosResult.success(scheduleBeanTransfer.toDTO(scheduleService.list()));
    }

    @ApiOperation(value = "根据id查询cron表达式")
    @GetMapping("{id}")
    public AxiosResult<ScheduleBeanDTO> findById(@PathVariable Long id) {
        return AxiosResult.success(scheduleBeanTransfer.toDTO(scheduleService.findById(id)));
    }

    @ApiOperation(value = "添加cron表达式")
    @PostMapping
    public AxiosResult<Void> add(@RequestBody ScheduleBean scheduleBean) {
        return toAxios(scheduleService.addSchedule(scheduleBean));
    }

    @PutMapping
    @ApiOperation(value = "修改定时任务")
    @HasPerm(perSign = "task:time:edit")
    public AxiosResult<Void> update(@RequestBody ScheduleBean scheduleBean) {
        //表单校验
        long cronId = scheduleBean.getCronId();
        ScheduledFuture scheduledFuture = map.get(cronId);
        //判断定时任务存在并且没有被取消
        if (scheduledFuture != null && !scheduledFuture.isCancelled())
            scheduledFuture.cancel(false);//先将定时任务取消
        map.remove(cronId);//将scheduledFuture从map中移除
        int i = scheduleService.updateSchedule(scheduleBean);
        //重新设置一个新的定时任务
        ScheduledFuture<?> schedule = threadPoolTaskScheduler.schedule(new Runnable() {
            @Override
            public void run() {
                List<Admin> admins = adminMapper.selectList(null);
                admins.forEach(admin -> emailService.sendMail(admin));
            }
        }, new CronTrigger(scheduleBean.getCronExpress()));
        //将定时任务存入map
        map.put(cronId, schedule);
        return toAxios(i);
    }

    @DeleteMapping("{id}")
    @ApiOperation(value = "通过id删除")
    @HasPerm(perSign = "task:time:delete")
    public AxiosResult<Void> deleteById(@PathVariable Long id) {
        int i = scheduleService.deleteById(id);
        ScheduledFuture scheduledFuture = map.get(i);
        if (scheduledFuture != null && !scheduledFuture.isCancelled())
            scheduledFuture.cancel(true);//取消定时任务

        map.remove(i);
        return toAxios(i);
    }

    @PutMapping("{id}")
    @ApiOperation(value = "暂停定时任务")
    @HasPerm(perSign = "task:time:pause")
    public AxiosResult<Void> pause(@PathVariable Long id) {
        ScheduledFuture scheduledFuture = map.get(id);
        if (scheduledFuture != null && !scheduledFuture.isCancelled())
            scheduledFuture.cancel(false);//停止定时任务
        map.remove(id);
        return AxiosResult.success();
    }

    @PostMapping("{id}")
    @ApiOperation(value = "重启")
    @HasPerm(perSign = "task:time:resume")
    public AxiosResult<Void> resume(@PathVariable Long id) {
        //从数据库中获取schedule对象
        ScheduleBean byId = scheduleService.findById(id);
        //根据cron表达式创建cronTrigger对象 , 并开启定时任务
        ScheduledFuture<?> schedule = threadPoolTaskScheduler.schedule(new Runnable() {
            @Override
            public void run() {
                List<Admin> admins = adminMapper.selectList(null);
                admins.forEach(admin -> emailService.sendMail(admin));
            }
        }, new CronTrigger(byId.getCronExpress()));
        map.put(byId.getCronId(), schedule);
        return AxiosResult.success();
    }

}

cn / utils / ReflectionUtils

/**
 * @className: ReflectionUtils
 * @description:
 */
@Slf4j //lombok提供 , 可以使用log来记录日志
public class ReflectionUtils {
    /**
     * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter
     */
    public static Object getFieldValue(Object object, String fieldName) {
        Field field = getDeclaredField(object, fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
        }
        makeAccessible(field);
        Object result = null;
        try {
            result = field.get(object);
        } catch (IllegalAccessException e) {
            log.error("ReflectionUtils类中 获取属性值错误");
        }

        return result;
    }

    /**
     * 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter
     */
    public static void setFieldValue(Object object, String fieldName, Object value) {
        Field field = getDeclaredField(object, fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
        }
        makeAccessible(field);
        try {
            field.set(object, value);
        } catch (IllegalAccessException e) {
            log.error("ReflectionUtils类中 设置属性值错误");
        }
    }


    /**
     * 通过反射, 获得定义 Class 时声明的父类的泛型参数的类型
     * 如: public EmployeeDao extends BaseDao<Employee, String>
     */
    @SuppressWarnings("unchecked")
    public static Class getSuperClassGenricType(Class clazz, int index) {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }

        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            return Object.class;
        }

        if (!(params[index] instanceof Class)) {
            return Object.class;
        }

        return (Class) params[index];
    }

    /**
     * 通过反射, 获得 Class 定义中声明的父类的泛型参数类型
     * 如: public EmployeeDao extends BaseDao<Employee, String>
     */
    @SuppressWarnings("unchecked")
    public static <T> Class<T> getSuperGenericType(Class clazz) {
        return getSuperClassGenricType(clazz, 0);
    }

    /**
     * 循环向上转型, 获取对象的 DeclaredMethod
     */
    public static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes) {

        for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
            try {
                return superClass.getDeclaredMethod(methodName, parameterTypes);
            } catch (NoSuchMethodException e) {
                //Method 不在当前类定义, 继续向上转型
                log.error("ReflectionUtils类中 目标对象中没有该方法");
            }
        }

        return null;
    }

    /**
     * 使 filed 变为可访问
     */
    public static void makeAccessible(Field field) {
        if (!Modifier.isPublic(field.getModifiers())) {
            field.setAccessible(true);
        }
    }

    /**
     * 循环向上转型, 获取对象的 DeclaredField
     */
    public static Field getDeclaredField(Object object, String filedName) {

        for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
            try {
                return superClass.getDeclaredField(filedName);
            } catch (NoSuchFieldException e) {
                //Field 不在当前类定义, 继续向上转型
                log.error("目标对象中没有这个属性");
            }
        }
        return null;
    }

    /**
     * 直接调用对象方法, 而忽略修饰符(private, protected)
     */
    public static Object invokeMethod(Object object, String methodName, Class<?>[] parameterTypes,
                                      Object[] parameters) {
        Method method = getDeclaredMethod(object, methodName, parameterTypes);
        if (method == null) {
            throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");
        }
        method.setAccessible(true);
        try {
            return method.invoke(object, parameters);
        } catch (Exception e) {
            log.error("方法执行失败");
        }

        return null;
    }
}

cn / utils / TreeUtils

菜单工具类

/**
 * @className: TreeUtils
 * @description: 动态生成菜单
 */
public class TreeUtils {


    /*
    参数一 : 最顶级节点的集合
    参数二 : 所有节点的集合
    目的 : 从所有数据中查询出顶级节点的子节点 , 递归查询 , 并按顺序输出
    流程 :
        1. 先对由由根节点组成的list进行排序
        2. 将排序后的list覆盖到排序前的
        3. 遍历根节点的list , 将子节点封装入根节点
     */
    public static <T> void buildTreeData(List<T> root, List<T> allList) {
        //将根节点进行排序
        List<T> sortToot = root.stream().sorted((t1, t2) -> {
            Integer sort1 = (Integer) ReflectionUtils.getFieldValue(t1, "sort");
            Integer sort2 = (Integer) ReflectionUtils.getFieldValue(t2, "sort");
            return sort1 - sort2;
        }).collect(Collectors.toList());
        //将排序后的list覆盖掉原list
        root.clear();
        root.addAll(sortToot);
        //从allList中找出跟节点的所有子节点 , 并封装入根节点
        root.forEach(t -> getChildren(t, allList));

    }

    /*
        方法作用 :
        将传入参数对象的 所有子级节点 和 子级节点的子级节点 以及更下级的节点 找出并封装入参数对象并排序

        流程 :
        1. 使用反射获取传入对象的id和list内所有对象的parentId (ReflectionUtils为反射工具类)
        2. 当list内对象的parentId与传入对象的id相等时 , 说明这个list内的对象是传入对象的下级对象
           使用list.stream().filter()将list内所有t的子级对象抽出单独生成一个list并返回
        3. 如果子级节点的list不为空 , 则根据子级节点的sort属性值对子级节点进行排序 , 使用递归寻找子级节点的子级节点
     */
    private static <T> void getChildren(T t, List<T> allList) {
        //找出所有的子级节点
        List<T> children = allList.stream().filter((t1) -> {
            Long id = (Long) ReflectionUtils.getFieldValue(t, "id");
            Long parentId = (Long) ReflectionUtils.getFieldValue(t1, "parentId");
            return id.equals(parentId);
        }).collect(Collectors.toList());
        //对子级节点排序 , 并使用递归找出更下级节点
        if (!CollectionUtils.isEmpty(children)) {
            //子节点排序
            List<T> sortedChildren = children.stream().sorted((t1, t2) -> {
                Integer sort1 = (Integer) ReflectionUtils.getFieldValue(t1, "sort");
                Integer sort2 = (Integer) ReflectionUtils.getFieldValue(t2, "sort");
                return sort1 - sort2;
            }).collect(Collectors.toList());
            //将子节点list封装入上级节点
            ReflectionUtils.setFieldValue(t, "children", sortedChildren);
            //递归寻找更下级节点
            sortedChildren.forEach(t1 -> getChildren(t1, allList));
        }
    }
}

cn / utils / UploadService

/**
 * @className: UploadService
 * @description: 头像上传
 */
@Component
public class UploadService {
    @Autowired
    private UploadProperties uploadProperties;

    public String upload(Part part) throws IOException {
        BufferedImage read = ImageIO.read(part.getInputStream());
        //判断上传的文件是不是图片
        if (read == null)
            throw new ApiException(AxiosStatus.NOT_IMG);
        //判断上传文件是否以jpg或png结尾
        if (uploadProperties.getUploadExt().contains(StringUtils.getFilenameExtension(part.getSubmittedFileName())))
            throw new ApiException(AxiosStatus.IMG_TYPE_ERROR);
        //获取文件大小 , 单位KB
        long size = part.getSize() / 1024;
        //判断文件上传大小是否合规
        if (size > uploadProperties.getUploadSize())
            throw new ApiException(AxiosStatus.IMG_TOO_LARGE);

        int width = read.getWidth();
        int height = read.getHeight();
        //判断图片长宽是否合规
        if (width != uploadProperties.getImgWidth() || height != uploadProperties.getImgHeight())
            throw new ApiException(AxiosStatus.IMG_SIZE_ERROR);
        //设置文件名
        String fileName = UUID.randomUUID().toString() + "." + StringUtils.getFilenameExtension(part.getSubmittedFileName());
        //获取服务器地址
        String endPoint = uploadProperties.getEndPoint();
        //获取登录id
        String accessKeyId = uploadProperties.getAccessKeyId();
        //获取登录密码
        String accessKeySecret = uploadProperties.getAccessKeySecret();
        //设置oss
        OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
        ossClient.putObject(uploadProperties.getBucket(), fileName, part.getInputStream());
        //关闭oss
        ossClient.shutdown();
        String url = uploadProperties.getBaseUrl() + fileName;
        return url;
    }
}
posted @ 2021-07-09 10:24  小_Leo  阅读(921)  评论(0编辑  收藏  举报