一款优雅漂亮的后台管理系统,让开发更快速,让你更注重业务流

介绍

INITSRC(启源)

INITSRC是一款面向个人、中小企业快速开发的开源前后端管理项目。用户可以基于该项目进行网站管理后台、商城、OA等开发和学习。该项目后台基于Springboot+Mybaits-plus+Shiro+Jwt等技术来实现;前端基于Vue+Router+Vuex+Axios等技术来实现。

项目演示

  • 项目地址:http://admin.initsrc.com
  • 账号密码:initsrc/a123456
  • 开发文档:docs.initsrc.com

联系我们

QQ技术交流群: 298264032

备注

承接各类软件开发,小程序、公众号、APP。有意向的联系微信:MISTAKEAI

技术介绍

服务端是基于Springboot、Shiro + JWT、Mybaits-plus + Pagehelper、Freemarker核心框架组成。

前端端是基于Vue、Router、Vuex、Element UI、Axios核心组件组成。

致力于协助初创科技企业建立技术储备、技术规范,让技术人员更加专注业务流,减少前期技术
搭建时间;此外,也帮助技术人员快速开发、减少重复工作。

应用环境

  1. JDK 1.8
  2. Apache Maven
  3. Servlet
  4. MYSQL8.0
  5. REDIS5.0

后端结构

initsrc    
├── initsrc-admin             // 后台管理端服务
│       └── module                         // 接口模块
│             └── controller               // 控制层
│             └── dao                      // 接口层
│             └── entity                   // 实体层
│             └── service                  // 实现层
├── initsrc-base              // 项目启动、properties总配置
├── initsrc-common            // 工具类
│       └── annotation                    // 自定义注解
│       └── base                          // 底层实体类
│       └── constant                      // 通用常量
│       └── controller                    // 工具类接口
│       └── enums                         // 通用枚举
│       └── exception                     // 通用异常
│       └── plugin                        // 第三方插件(redis、OSS)
│       └── util                          // 通用类处理
├── core                     // 框架核心
│       └── aspects                       // 注解实现
│       └── biz                           // 系统业务层
│       └── filter                        // 系统过滤层
│       └── module                        // 系统依赖模块(shiro、mybaits-plus)
├── initsrc-devtool         // 开发工具(不用可移除)
├── initsrc-monitor         // 系统监控(不用可移除)
├── initsrc-xxxxxx          // 其他模块

前端结构

initsrc-web    
├── src           
│    └── api                         // axios接口封装
│    └── assets                      // js、img、css
│    └── components                  // 自定义组件
│    └── layout                      // 页面布局
│    └── plugins                     // 第三方组件、自定义组件注册
│    └── router                      // 路由控制
│    └── store                       // vuex临时存储控制
│    └── views                       // 业务页面管理

后端技术选型

技术 版本 说明
Spring Boot 2.3.0.RELEASE 容器+MVC框架
Shiro 1.4.0 认证和授权框架
JWT 3.3.0 无状态认证协议
MyBatis-plus 3.3.2 ORM框架
Pagehelper 5.1.10 分页插件
Freemarker 2.3.28 代码生成引擎
Springfox-Swagger2 2.9.2 API文档管理
Redis 5.0 分布式缓存
Druid 1.1.10 数据库连接池
Lombok 1.18.6 简化对象封装工具
Oshi-Core 3.9.1 获取应用服务信息
P6spy 3.8.0 针对数据库访问操作的动态监测框架

核心技术介绍

Springboot框架

  1. 介绍

Spring Boot 是由 Pivotal 团队提供的全新框架,2014 年 4 月发布 Spring Boot 1.0 2018 年 3 月 Spring Boot 2.0发布。它是对spring的进一步封装,其设计目的是用来简化 Spring 应用的初始搭建以及开发过程。怎么简化的呢?就是通过封装、抽象、提供默认配置等方式让我们更容易使用。

SpringBoot 基于 Spring 开发。SpringBoot 本身并不提供 Spring 框架的核心特性以及扩展功能,也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。

关于 SpringBoot 有一句很出名的话就是约定大于配置。采用 Spring Boot 可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis、MongoDB、Jpa、kafka,Hakira 等等。SpringBoot 应用中这些第三方库几乎可以零配置地开箱即用,大部分的 SpringBoot 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。

扫描二维码关注公众号,回复: 15101682 查看本文章
  1. SpringBoot优点
  • 创建独立的Spring应用程序

  • 嵌入的Tomcat,无需部署WAR文件

  • 提供一个starter POMs来简化Maven配置

  • 尽可能自动配置Spring

  • 提供生产就绪型功能,如指标,健康检查和外部配置

  • 绝对没有代码生成并且对XML也没有配置要求

Shiro + JWT

  1. Shiro 介绍

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

  1. JWT 介绍

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和

Mybaits-plus + Pagehelper

  1. Mybaits-plus 介绍

MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

  1. 特性
  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑

  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作

  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求

  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错

  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题

  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作

  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )

  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用

  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询

  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库

  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询

  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

  1. PageHelper

PageHelper就是mybatis拦截器的一个应用,实现分页查询,支持常见的 12 种数据库的物理分页并支持多种分页方式。

Freemarker

  1. 介绍

Apache FreeMarker是一款开源的模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板使用FreeMarker Template Language(FTL)模板语言编写,这是一种简单的专用语言。模板用于展示数据,数据模型用于呈现什么数据。

  1. 特性
  • 强大的模板语言

  • 多用途且轻量

  • 智能的国际化和本地化

  • XML处理能力

  • 通用的数据模型

前端技术选型

技术 版本 说明
Vue 2.6.11 一套用于构建用户界面的渐进式框架
vue-router 3.2.0 路由管理器
vuex 3.4.0 状态管理模式
Element-ui 2.14.1 前端UI库
Axios 0.21.1 一个基于 promise 的 HTTP 库
vue-apexcharts 1.6.0 统计视图库
Xterm 4.12.0 终端模拟器
@riophae/vue-treeselect 0.4.0 树形选择器

核心技术介绍

Vue

  1. 介绍

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

Router

  1. 介绍

Vue Router 是 Vue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

Vuex

  1. 介绍

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Element UI

  1. 介绍

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

  1. 特性
  • 一致性 Consistency
    与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;
    在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。

  • 反馈 Feedback
    控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;
    页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。

  • 效率 Efficiency
    简化流程:设计简洁直观的操作流程;
    清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;
    帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。

  • 可控 Controllability
    用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;
    结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。

Axios

  1. 介绍

axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它本身具有以下特征:

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止 CSRF/XSRF

前后端手册

后端获取用户信息方式

本项目为了方便用户在既定的方法里获取用户相关数据,例如用户ID,系统信息。我们运用了AOP方式进行参数注入,通过用户
在指定方法上添加@LoginUser,即可获取用户登录信息。

  1. 配置方式

新增注解

package com.initsrc.common.annotation;
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoginUser {
}

注解实现

package com.initsrc.core.aspects;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.initsrc.common.base.LoginInfo;
import com.initsrc.common.base.RedisInfo;
import com.initsrc.common.base.Result;
import com.initsrc.common.constant.AuthConstant;
import com.initsrc.common.enums.ResultEnum;
import com.initsrc.common.exception.BusinessException;
import com.initsrc.common.plugin.redis.RedisImpl;
import com.initsrc.common.util.jwt.JwtUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
 * AOP获取用户登录信息
 * 作者:INITSRC (启源)
 */
@Aspect
@Component
public class LoginUserAspects {
    @Resource
    private RedisImpl redisImpl;
    @Pointcut("@annotation(com.initsrc.common.annotation.LoginUser)")
    public void LoginUserImpl() {
    }
    @Before("LoginUserImpl()")
    public void LoginUserImpl(JoinPoint joinPoint) throws Throwable {
        Object[] argc = joinPoint.getArgs();
        Class clazz;
        Method[] methods;
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attr.getRequest();
        String token = request.getHeader("token");
        if(token == null){
            throw new BusinessException(ResultEnum.CODE_402.getCode(),ResultEnum.CODE_402.getMsg());
        }
        String userId = null;
        String scopeType = null;
        String scopeId = null;
        String scopeIds = null;
        if(token.equals("INITSRC")){
            userId = "1";
            scopeType = "0";
        }else {
            String account = JwtUtil.getClaim(token, AuthConstant.TOKEN_ACCOUNT);
            RedisInfo<LoginInfo> info = JSON.parseObject(JSON.toJSONString(redisImpl.get(AuthConstant.REDIS_ACCOUNT_KEY + account)), new TypeReference<RedisInfo<LoginInfo>>() {
            });
            if (info == null) {
              throw new BusinessException(ResultEnum.CODE_401.getCode(),ResultEnum.CODE_401.getMsg());
            }
            userId = info.getLoginInfo().getUid();
            scopeType = info.getLoginInfo().getIsSearch();
            scopeId = info.getLoginInfo().getDepartmentId();
            scopeIds = info.getLoginInfo().getPowerDepts();
        }
        for (Object object : argc) {
            if (null == object) {
                continue;
            }
            clazz = object.getClass();
            methods = clazz.getMethods();
            // 这里的methods会包含父类的public方法,也包括Object类的method
            for (Method method : methods) {
                if (method.getName().equals("setAuthId")) {
                    method.invoke(object,userId);
                }else if (method.getName().equals("setScopeType")) {
                    method.invoke(object,scopeType);
                }else if (method.getName().equals("setScopeId")) {
                    method.invoke(object,scopeId);
                }else if (method.getName().equals("setScopeIds")) {
                    method.invoke(object,scopeIds);
                }
            }
        }
    }
}

后端日志记录方式

本项目的操作日志采用AOP进行记录操作记录;并且用户可以通过添加注解形式进行相对于操作的记录。

  1. 配置方式
package com.initsrc.common.annotation;
import com.initsrc.common.enums.LogOperateTypeEnum;
import java.lang.annotation.*;
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
    /**
     * 操作类型(enum):添加,删除,修改,登陆
     */
    LogOperateTypeEnum operationType();

    //操作内容(content)
    String operateContent();
}
package com.initsrc.core.aspects;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.google.common.collect.Maps;
import com.initsrc.common.annotation.LogAnnotation;
import com.initsrc.common.base.LoginInfo;
import com.initsrc.common.base.LoginResultVo;
import com.initsrc.common.base.RedisInfo;
import com.initsrc.common.constant.AuthConstant;
import com.initsrc.common.enums.LogOperateTypeEnum;
import com.initsrc.common.plugin.address.IpToAddressUtil;
import com.initsrc.common.plugin.redis.RedisImpl;
import com.initsrc.common.util.IpUtil;
import com.initsrc.common.util.ServletUtils;
import com.initsrc.common.util.jwt.JwtUtil;
import com.initsrc.core.biz.entity.SysLogCore;
import com.initsrc.core.biz.service.SysLogService;
import eu.bitwalker.useragentutils.UserAgent;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
 * 日志AOP实现
 * 作者:INITSRC (启源)
 */
@Aspect
@Component
public class LogAspects {
    @Resource
    private SysLogService sysLogService;
    @Resource
    private RedisImpl redis;

    //只要使用了该注解,就会进入切面
    @Pointcut("@annotation(com.initsrc.common.annotation.LogAnnotation)")
    public void operationLog() {
    }
    /**
     * 方法返回之后调用
     *
     * @param joinPoint
     * @param returnValue 方法返回值
     */
    @AfterReturning(value = "operationLog()", returning = "returnValue")
    public void doAfter(JoinPoint joinPoint, Object returnValue) {
        getLogVo(joinPoint, returnValue);
    }
    private void getLogVo(JoinPoint joinPoint, Object returnValue) {
        //登录的token中去拿当前登录的用户Id
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attr.getRequest();
        RedisInfo<LoginInfo> loginInfo = null;
        //获取token值
        String token = request.getHeader("token");
        if (token != null) {
            String account = JwtUtil.getClaim(token, AuthConstant.TOKEN_ACCOUNT);
            loginInfo = JSON.parseObject(JSON.toJSONString(redis.get(AuthConstant.REDIS_ACCOUNT_KEY + account)), new TypeReference<RedisInfo<LoginInfo>>() {
        });
        }
        SysLogCore logVo = new SysLogCore();
        //获取类名称
        String targetName = joinPoint.getTarget().getClass().getName();
        Class targetClass = null;
        LogAnnotation logAnnotation = null;
        try {
            //反射
            targetClass = Class.forName(targetName);
            //获得切入点所在类的所有方法
            Method[] methods = targetClass.getMethods();
            //获取切入点的方法名称
            String methodName = joinPoint.getSignature().getName();
            //获取切入点的参数
            Object[] arguments = joinPoint.getArgs();

            //遍历方法名
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    Class[] clazzs = method.getParameterTypes();
                    //比较声明的参数个数和传入的是否相同
                    if (clazzs.length == arguments.length) {
                        //获取切入点方法上的注解
                        logAnnotation = method.getAnnotation(LogAnnotation.class);
                        break;
                    }
                }
            }
            if (returnValue != null && returnValue.getClass() != HashMap.class) {
                Map<String, Object> map = Maps.newHashMap();
                if (returnValue != null) {
                    BeanMap beanMap = BeanMap.create(returnValue);
                    for (Object key : beanMap.keySet()) {
                        map.put(key + "", beanMap.get(key));
                    }
                }
                logVo.setRequestResult(JSON.toJSONString(map));
                if(map.get("code").toString().equals("0")){
                    logVo.setStatus("1");
					//判断是否是登录操作
                    if (logAnnotation.operationType().equals(LogOperateTypeEnum.LOGIN)) {
                        LoginResultVo loginVo = JSONObject.parseObject(JSON.toJSONString(map.get("data")), LoginResultVo.class);
                        String account = JwtUtil.getClaim(loginVo.getToken(), AuthConstant.TOKEN_ACCOUNT);
                        if (loginVo != null) {
                            loginInfo = JSON.parseObject(JSON.toJSONString(redis.get(AuthConstant.REDIS_ACCOUNT_KEY + account)), new TypeReference<RedisInfo<LoginInfo>>() {
                            });
                        } else {
                            logVo.setUserId(null);
                        }

                    }
                }else{
                    logVo.setStatus("0");
                    logVo.setErrorMsg(map.get("msg").toString());
                }
            }
            if (loginInfo != null) {
                //请求用户ID
                logVo.setUserId(loginInfo.getLoginInfo().getUid());
                //请求用户
                logVo.setRequestName(loginInfo.getLoginInfo().getNickName());
                //为日志实体类赋值
                logVo.setBizType(String.valueOf(logAnnotation.operationType().getOperateCode()));
                //忽略shiro返回的数据
                logVo.setTitle(logAnnotation.operateContent());
                //请求路径
                logVo.setRequestUrl(ServletUtils.getRequest().getRequestURI());
                //请求参数
                logVo.setRequestParam(JSON.toJSONString(request.getParameterMap()));
                // 设置方法名称
                String className = joinPoint.getTarget().getClass().getName();
                logVo.setMethod(className + "." + methodName + "()");
                // 设置请求方式
                logVo.setRequestType(ServletUtils.getRequest().getMethod());
                //获取ip地址
                logVo.setRequestIp(IpUtil.getIpAddr(request));
                //平台类型
                logVo.setPlatformType(loginInfo.getPlf());
                UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("user-agent"));
                logVo.setOs(userAgent.getOperatingSystem().getDeviceType().toString());
                logVo.setBrowser(userAgent.getBrowser().toString());
                logVo.setRequestAdress(IpToAddressUtil.getCityInfo(logVo.getRequestIp()));
                //添加日志
                sysLogService.save(logVo);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  1. 实现示例
    在controller的方法上添加
@GetMapping("/page")
@LogAnnotation(operationType = LogOperateTypeEnum.SEARCH, operateContent = "查询数据")
public Result<PageResult<SysPermListVo>> pageData(){
}

后台事务注解

  1. 配置方式
    通过注解的方式(也可以通过xml或者Java配置类的方式,不过没有使用注解的方式快)开启你的SpringBoot应用对事务的支持。
    使用@EnableTransactionManagement注解(来自于上面引入的spring-tx包)

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

@SpringBootApplication
@EnableTransactionManagement
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}
  1. 实现示例
/**
 * 测试Spring事务只拦截 RuntimeException 和 Error
 * 成功插入user
 */
@Transactional
@PostMapping("/testTransactionNoExcep")
public User testTransactionNoExcep() {
	User user = new User();
	user.setName("小李");
	user.setAge((short) 13);
	user.setCity("北京");
	userMapper.insert(user);
	return user;
}

/**
 * 测试Spring事务 拦截所有异常 Exception
 * 成功插入user
 */
@Transactional(rollbackFor = Exception.class)
@PostMapping("/testTransactionNoExcep")
public User testTransactionNoExcep() throws Exception{
	User user = new User();
	user.setName("小李");
	user.setAge((short) 13);
	user.setCity("北京");
	userMapper.insert(user);
	return user;
}

后台全局异常捕捉

  1. 权限异常捕捉
/**
 * 描述:异常捕捉类
 * 作者:INITSRC
 *
 */
@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler(Exception.class)  //申明捕获哪个异常类
    public Map<String, Object> handlerException(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", ResultEnum.CODE_500.getCode());
        map.put("msg", ResultEnum.CODE_500.getMsg());
        String desc = StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : e.toString();
        map.put("desc", desc);
        log.error("全局拦击,具体信息为:{}", e.getMessage());
        return map;
    }
	....
}
  1. 自定义业务异常
/**
 * 描述:业务异常
 * 作者:INITSRC
 */
@Data
public class BusinessException extends RuntimeException {
    //异常处理编码
    private Integer code;
   //异常处理信息
    private String msg;
    //异常处理描述
    private String desc;
    public BusinessException(Integer code, String msg, String desc) {
        this.code = code;
        this.msg = msg;
        this.desc = desc;
    }
    public BusinessException(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public BusinessException(String msg) {
        this.code = ResultEnum.CODE_1.getCode();
        this.msg = msg;
    }
}
//统一捕捉返回
@ExceptionHandler(BusinessException.class)
public Map<String, Object> handlerBusinessException(BusinessException e) {
	Map<String, Object> map = new HashMap<>();
	map.put("code", e.getCode());
	map.put("msg", e.getMsg());
	map.put("desc", e.getDesc());
	log.error("业务异常,具体信息为:{}", e.getMessage());
	return map;
}

后台参数验证

后台采用了@Validated来校验数据,并通过全局异常来捕获处理

  1. 实现示例
//在Entity前添加@Validated
@GetMapping("/testValidated")
public Result pageData(@Validated SysPermSaveDto dto){
}
//在Entity里通过@NotNull @NotBlank等注解实现。
@Data
public class SysPermSaveDto  implements Serializable {
	 @ApiModelProperty(value = "菜单名称")
    @NotBlank(message = "菜单名称不能为空")
    private String name;

    @ApiModelProperty(value = "菜单路径")
    @NotNull(message = "菜单路径不能为空")
    private String path;
}
  1. 异常捕捉
@ExceptionHandler(BindException.class)
    public Map<String, Object> BindException(BindException e) {
        //业务处理
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, Object> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
	//业务处理
}

后端数据查询权限

本项目采用AOP拦截方式进行对应权限的SQL的生成并通过参数形式在myabits的xml里进行拼接。
本项目的权限是根据组织架构进行数据权限控制。权限分别为:

  • 全部权限
  • 自定义部门权限
  • 本部门及以下权限
  • 本部门权限
  1. 配置方式

新增注解

package com.initsrc.common.annotation;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 *
 * @author INITSRC (启源)
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {

    /**
     * 部门表的别名
     */
    String deptAlias() default "";
}

注解实现

package com.initsrc.core.aspects;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.initsrc.common.annotation.DataScope;
import com.initsrc.common.base.BaseEntity;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 数据权限拼接处理类
 *
 * @author INITSRC (启源)
 */
@Aspect
@Component
public class DtaScopeAspects {
    /**
     * 全部查询权限
     */
    public static final String DATA_SCOPE_ALL = "0";

    /**
     * 全部查询权限
     */
    public static final String DATA_SCOPE_CUSTOMIZE = "1";

    /**
     * 本部门及以下
     */
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "2";

    /**
     * 本部门
     */
    public static final String DATA_SCOPE_DEPT = "3";

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

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

    @Before("dataScopePointCut()")
    public void doBefore(JoinPoint point) throws Throwable {
        handleDataScope(point);
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    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;
    }

    protected void handleDataScope(final JoinPoint joinPoint) {
        // 获得注解
        // 获得注解
        DataScope dataScope = getAnnotationLog(joinPoint);
        if (dataScope == null) {
            return;
        }
        this.dataScopeFilter(joinPoint, dataScope);
    }

    /**
     * 数据范围过滤
     *
     * @param joinPoint 切点
     */
    private void dataScopeFilter(JoinPoint joinPoint, DataScope dataScope) {
        StringBuilder sqlString = new StringBuilder();
        String deptAlias = dataScope.deptAlias();

        BaseEntity baseDto = (BaseEntity) joinPoint.getArgs()[0];

        String isSearch = String.valueOf(baseDto.getScopeType());
        if (DATA_SCOPE_ALL.equals(isSearch)) {
            sqlString = new StringBuilder();
        } else if (DATA_SCOPE_CUSTOMIZE.equals(isSearch)) {
            sqlString.append(StringUtils.format(" AND %s.dept_id in (%s) ", deptAlias,baseDto.getScopeIds()));
        } else if (DATA_SCOPE_DEPT_AND_CHILD.equals(isSearch)) {
            //本公司及其子部门(部门主管)
            sqlString.append(StringUtils.format(" AND %s.dept_id in (SELECT dept_id FROM is_sys_dept WHERE find_in_set( '%s' , search_code )) ", deptAlias, baseDto.getScopeId()));
        } else if (DATA_SCOPE_DEPT.equals(isSearch)) {
            sqlString.append(StringUtils.format(" AND %s.dept_id = '%s' ", deptAlias, baseDto.getScopeId()));
        } else {
            sqlString.append(StringUtils.format(" AND 1=0 "));

        }

        baseDto.getParams().put(DATA_SCOPE, sqlString.toString());

    }
}
  1. 实现示例

在service需要设置权限的方法上添加,其中a代表的是表的别名。此后在mapper.xml里要控制权限的sql语句底部添加${params.dataScope}

@DataScope(deptAlias = "a")

<select id="xxx">
select * from xxx where 1=1 
${params.dataScope}
</select>

前后端权限解析

  • 后端采用Shiro的 @RequiresPermissions 注解进行权限控制
  1. 权限过滤核心

在Shiro自定义的AuthorizingRealm里实现

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
	//获取token
	String token = principals.toString();
	SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
	//根据token获取用户名
	String account = JwtUtil.getClaim(token, AuthConstant.TOKEN_ACCOUNT);
	//根据用户名获取redis存储的权限
	RedisInfo<LoginInfo> loginScRedisInfo = JSON.parseObject(JSON.toJSONString(redisImpl.get(AuthConstant.REDIS_ACCOUNT_KEY + account)), new TypeReference<RedisInfo<LoginInfo>>() {
	});
	if (loginScRedisInfo != null) {
		//注册权限
		loginScRedisInfo.getLoginInfo().getLoginPermVos().forEach(item -> {
			simpleAuthorizationInfo.addStringPermission(item.getPerm());
		});
	} else {
		throw new AuthenticationException("凭证过期");
	}
	return simpleAuthorizationInfo;
}
  1. 实现示例

在controller的方法上添加

@GetMapping("/page")
@RequiresPermissions("p:system:perm:page")
public Result<PageResult<SysPermListVo>> pageData(){
}
  • 前端权限控制
  1. 前端获取权限格式
[
	{
		"color": "",  //按钮颜色
		"component": "", //VUE组件注册
		"icon": "", //图标
		"isCache": "", //是否缓存
		"linkType": "", //链接类型
		"name": "", //权限名称
		"path": "", //前端路径
		"perm": "", //后端 shiro 权限
		"resource": "", //权限类型 0:菜单 1:按钮 2:表格按钮
		"sort": 0 //排序
	}
]
  1. 权限按钮转换工具
// 权限按钮过滤设置
  powerSet: function(data, that) {
    let obj = {
      tableOper: [],
      headerOper: []
    }
    if (data != null) {
      if (data.length > 0) {
        data.forEach(function(item, index) {
          var btn = {
            id: index,
            label: item.name,
            type: item.color,
            perm: item.perm,
            show: true,
            icon: item.icon,
            plain: true,
            disabled: false,
            method: (index, row) => {
              let list = item.perm.split(":")
              if (list[list.length - 1] == "add") {
                that.handleAdd(item.path)
              } else if (list[list.length - 1] == "edit") {
                that.handleEdit(index, row, item.path)
              } else if (list[list.length - 1] == "detail") {
                that.handleDetail(index, row, item.path)
              } else if (list[list.length - 1] == "del") {
                that.handleDel(index, row)
              } else if (list[list.length - 1] == "dels") {
                that.handleDels()
              } else if (list[list.length - 1] == "import") {
                that.handleImport(item.path)
              } else if (list[list.length - 1] == "export") {
                that.handleExport(item.path)
              } 
            }
          }
          if (item.resource == 2) {
            obj.tableOper.push(btn);
          } else {
            obj.headerOper.push(btn)
          }
        })
      }
    }
    return obj
  },
  1. 实现示例
    通过router.afterEach获取当前菜单的权限数组对象。
router.afterEach((to, from, next) => {
  store.commit("_SET_ACTION", to) //VUEX实现
})
//vuex里mutation方法实现
 _SET_ACTION(state, value) {
 let forEc = function(data, to) {
   data.forEach(function(c) {
	 if (c.path == to.path) {
	   if (c.resource == 0) {
		 state.PERM_BTN = c.children;
	   }
	 }
	 if (c.children != null) {
	   if (c.children.length > 0) {
		 forEc(c.children, to);
	   }
	 }
   })
 }
 //遍历后端权限数组,获取当前路由对象,并赋予state.PERM_BTN
 forEc(state.ROUTER_MENU, value)
 }

通过进入页面的mounted,执行权限转换工具获取对应按钮权限。

提示:为什么不做全局,是因为每个页面的权限可能会进行一些特殊处理。

mounted() {
  let that = this
  //权限初始化
  var oper = this.powerCommon.powerSet(that.$store.state.ps.PERM_BTN, that);
  that.operates.list = oper.tableOper
  that.headBtn = oper.headerOper
}

前后端分页解析

前端分页采用了Eelement的table组件和Pagination组件封装的is-table。后端采用了Pagehelper插件。

  1. 前端示例
  • table封装代码
  /**
  * 作者:大神很烦恼
  * 邮箱:[email protected]
  * 昵称:INITSRC
  */
  <!--region 封装的分页 table-->
  <template>
    <div>
      <el-table id="iTable" v-loading.iTable="options.loading" :element-loading-background="THEAM.THEAM_TABLE.tableloading"
        :data="list" :height="options.isFixed && THEAM.ISDEVICE==0?height:null" :stripe="options.stripe" ref="mutipleTable" @selection-change="handleSelectionChange"
        row-key="id" :default-expand-all="options.isOpen" :tree-props="{children: options.children, hasChildren: options.hasChildren}"
        :header-cell-style="THEAM.THEAM_TABLE">
        <template slot="empty">
          <div style="width: 100%;padding-top: 50px;">
            <img class="data-pic" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDEiIHZpZXdCb3g9IjAgMCA2NCA0MSIgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAxKSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgIDxlbGxpcHNlIGZpbGw9IiNGNUY1RjUiIGN4PSIzMiIgY3k9IjMzIiByeD0iMzIiIHJ5PSI3Ii8+CiAgICA8ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0iI0Q5RDlEOSI+CiAgICAgIDxwYXRoIGQ9Ik01NSAxMi43Nkw0NC44NTQgMS4yNThDNDQuMzY3LjQ3NCA0My42NTYgMCA0Mi45MDcgMEgyMS4wOTNjLS43NDkgMC0xLjQ2LjQ3NC0xLjk0NyAxLjI1N0w5IDEyLjc2MVYyMmg0NnYtOS4yNHoiLz4KICAgICAgPHBhdGggZD0iTTQxLjYxMyAxNS45MzFjMC0xLjYwNS45OTQtMi45MyAyLjIyNy0yLjkzMUg1NXYxOC4xMzdDNTUgMzMuMjYgNTMuNjggMzUgNTIuMDUgMzVoLTQwLjFDMTAuMzIgMzUgOSAzMy4yNTkgOSAzMS4xMzdWMTNoMTEuMTZjMS4yMzMgMCAyLjIyNyAxLjMyMyAyLjIyNyAyLjkyOHYuMDIyYzAgMS42MDUgMS4wMDUgMi45MDEgMi4yMzcgMi45MDFoMTQuNzUyYzEuMjMyIDAgMi4yMzctMS4zMDggMi4yMzctMi45MTN2LS4wMDd6IiBmaWxsPSIjRkFGQUZBIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K"
              alt="" />
          </div>
          <div style="line-height: 0px;padding: 10px 10px 50px 10px;">
            <span>暂无数据</span>
          </div>
        </template>
        <!--region 选择框-->
        <el-table-column v-if="options.mutiSelect" type="selection" style="width: 55px;" v-bind:selectable="options.checkstu"></el-table-column>
        <!--endregion-->
        <!--region 数据列-->
        <el-row :gutter="24" v-if="THEAM.ISDEVICE==1">
          <el-table-column key="内容" label="内容">
            <template slot-scope="scope">
              <template v-for="(column, index) in columns">
                <el-col :span="24">
                  <div class="dshfn-label-item">
                    <label class="dshfn-label-item__label" style="width: unset !important;">{
   
   {column.label}}</label>
                    <label class="dshfn-label-item__content">
                      <template v-if="!column.render">
                        <template v-if="column.formatter">
                          <span v-html="column.formatter(scope.row, column,scope.row[column.prop])"></span>
                        </template>
                        <template v-else>
                          <span>{
   
   {scope.row[column.prop]}}</span>
                        </template>
                      </template>
                      <template v-else>
                        <expand-dom :column="column" :row="scope.row" :render="column.render" :index="index"></expand-dom>
                      </template>
                    </label>
                  </div>
                </el-col>
              </template>
              <el-col :span="24">
                <div class="operate-group dshfn-label-item">
                  <div class="dshfn-label-item__content">
                    <template v-for="(btn, key) in operates.list">
                      <div class="op-item" v-if="(btn.show != true && btn.show !=false)?btn.show(scope.$index,scope.row):btn.show"
                        :key="btn.id">
                        <el-button :type="btn.type" size="mini" :icon="btn.icon" :disabled="(btn.disabled != true && btn.disabled !=false)?btn.disabled(scope.$index,scope.row):btn.disabled"
                          :plain="btn.plain" @click.native.prevent="btn.method(key,scope.row)">{
   
   { btn.label }}</el-button>
                      </div>
                    </template>
                  </div>
                </div>
              </el-col>
            </template>
          </el-table-column>
        </el-row>
        <template v-for="(column, index) in columns"  v-else>
          <el-table-column :prop="column.prop" :key="column.label" :label="column.label" :align="column.align" :fixed="column.fixed"
            :width="column.width">
            <template slot-scope="scope">
              <template v-if="!column.render">
                <template v-if="column.formatter">
                  <span v-html="column.formatter(scope.row, column,scope.row[column.prop])"></span>
                </template>
                <template v-else>
                  <span>{
   
   {scope.row[column.prop]}}</span>
                </template>
              </template>
              <template v-else>
                <expand-dom :column="column" :row="scope.row" :render="column.render" :index="index"></expand-dom>
              </template>
            </template>
          </el-table-column>
        </template>
        <!--endregion-->
        <!--region 按钮操作组-->
        <el-table-column ref="fixedColumn" label="操作" :width="operates.width" :fixed="operates.fixed" v-if="operates.list.length > 0 && THEAM.ISDEVICE==0">
          <template slot-scope="scope">
            <div class="operate-group">
              <template v-for="(btn, key) in operates.list">
                <div v-if="key < 3">
                  <div class="op-item"  v-if="(btn.show != true && btn.show !=false)?btn.show(scope.$index,scope.row):btn.show"
                    :key="btn.id">
                    <el-button :type="btn.type" size="mini" :icon="btn.icon" :disabled="(btn.disabled != true && btn.disabled !=false)?btn.disabled(scope.$index,scope.row):btn.disabled"
                      :plain="btn.plain" @click.native.prevent="btn.method(key,scope.row)">{
   
   { btn.label }}</el-button>
                  </div>
               </div>
              </template>
              <div v-if="operates.list.length >3">
                <el-dropdown style="padding-left: 5px;">
                  <el-button type="text" size="mini">
                    更多
                    <i class="el-icon-arrow-down el-icon--right"></i>
                  </el-button>
                  <el-dropdown-menu slot="dropdown">
                    <template v-for="(btn, key) in operates.list">
                      <div v-if="key >=3">
                        <div v-if="(btn.show != true && btn.show !=false)?btn.show(scope.$index,scope.row):btn.show">
                          <el-dropdown-item @click.native.prevent="btn.method(key,scope.row)" :disabled="btn.disabled">{
   
   { btn.label }}</el-dropdown-item>
                        </div>
                      </div>
                    </template>
                  </el-dropdown-menu>
                </el-dropdown>
              </div>
            </div>
          </template>
        </el-table-column>
        <!--endregion-->
      </el-table>
      <!--region 分页-->
      <div style=" padding: 10px;float: right;">
        <el-pagination v-if="pagination" @size-change="handleSizeChange" @current-change="handleIndexChange" :page-size="tableCurrentPagination.pageSize"
          :page-sizes="this.tableCurrentPagination.pageArray" :current-page="tableCurrentPagination.pageIndex" layout="total,sizes, prev, pager, next,jumper"
          :total="total"></el-pagination>
      </div>
      <!--endregion-->
    </div>
  </template>
  
  <script>
    const _pageArray = [10, 20, 50, 100]; // 每页展示条数的控制集合
    export default {
      props: {
        list: {
          type: Array,
          default: [] // prop:表头绑定的地段,label:表头名称,align:每列数据展示形式(left, center, right),width:列宽
        }, // 数据列表
        columns: {
          type: Array,
          default: [] // 需要展示的列 === prop:列数据对应的属性,label:列名,align:对齐方式,width:列宽
        },
        operates: {
          type: Object,
          default: {} // width:按钮列宽,fixed:是否固定(left,right),按钮集合 === label: 文本,type :类型(primary / success / warning / danger / info / text),show:是否显示,icon:按钮图标,plain:是否朴素按钮,disabled:是否禁用,method:回调方法
        },
        total: {
          type: Number,
          default: 0
        }, // 总数
        pagination: {
          type: Object,
          default: null // 分页参数 === pageSize:每页展示的条数,pageIndex:当前页,pageArray: 每页展示条数的控制集合,默认 _page_array
        },
        options: {
          type: Object,
          default: {
            stripe: false, // 是否为斑马纹 table
            loading: false, // 是否添加表格loading加载动画
            highlightCurrentRow: false, // 是否支持当前行高亮显示
            mutiSelect: false, // 是否支持列表项选中功能
            isFixed: false, //是否固定高
            isOpen: false, //是否默认树形table展开
            children: "children", //树形table的子节点名称
            hasChildren: "hasChildren",
            tableHeight: 200, //如果固定高,默认200px
            checkstu: function(row, index) { //选择按钮禁用或启用
              return true;
            }
          }
        } // table 表格的控制参数
      },
      components: {
        expandDom: {
          functional: true,
          props: {
            row: Object,
            render: Function,
            index: Number,
            column: {
              type: Object,
              default: null
            }
          },
          render: (h, ctx) => {
            const params = {
              row: ctx.props.row,
              index: ctx.props.index
            };
            if (ctx.props.column) params.column = ctx.props.column;
            return ctx.props.render(h, params);
          }
        }
      },
      data() {
        return {
          pageIndex: 1,
          tableCurrentPagination: {},
          multipleSelection: [], // 多行选中
          height: 200,
          THEAM:null
        };
      },
      mounted() {
        if (this.pagination && !this.pagination.pageSizes) {
          this.pagination.pageArray = _pageArray; // 每页展示条数控制
        }
        this.tableCurrentPagination = this.pagination || {
          pageSize: this.total,
          pageIndex: 1
        }; // 判断是否需要分页
      },
      methods: {
        // 切换每页显示的数量
        handleSizeChange(size) {
          if (this.pagination) {
            this.tableCurrentPagination = {
              pageIndex: 1,
              pageSize: size,
              pageArray: _pageArray // 每页展示条数控制
            };
            this.$emit("handleSizeChange", this.tableCurrentPagination);
          }
        },
        // 切换页码
        handleIndexChange(currnet) {
          if (this.pagination) {
            this.tableCurrentPagination.pageIndex = currnet;
            this.$emit("handleIndexChange", this.tableCurrentPagination);
          }
        },
        // 多行选中
        handleSelectionChange(val) {
          this.multipleSelection = val;
          this.$emit("handleSelectionChange", val);
        },
        //获取table 高
        getHeight(data) {
          if(window.innerWidth < 769){
            this.THEAM.ISDEVICE = 1
          }
          if (null != data && data.type == null) {
            this.height = window.innerHeight - data;
          } else {
            this.height = window.innerHeight - this.options.tableHeight;
          }
        },
      },
      created() {
        this.THEAM = this.$store.state.ts
        window.addEventListener("resize", this.getHeight);
        this.getHeight(null);
      },
      destroyed() {
        window.removeEventListener("resize", this.getHeight);
      },
    }
  </script>
  
  <style scoped>
    .op-item {
      float: left;
      padding-left: 10px;
    }
    /* 解决element-ui的table表格控件表头与内容列不对齐问题 */
    /deep/ .el-table th.gutter {
      display: table-cell !important;
      width: 15px !important;
    }
  </style>
  • 在组件中使用
<template>
  <i-table :list="list" :total="total" :options="options" :pagination="pagination" :columns="columns" :operates="operates"
	@handleSizeChange="handleSizeChange" @handleIndexChange="handleIndexChange" @handleSelectionChange="handleSelectionChange"
	ref="iTable">
  </i-table>
</template>
<script>
export default {
	data() {
	  return {
		  //选择的数据
		  multipleId: [],
		  list:[],
		  columns:[],
		  operates: {
		    width: 150,
		    fixed: 'right',
		    list: []
		 },
		 //数据总数量
		 total: 12,
		 //设置分页参数
		 pagination: {
		   pageIndex: 1,
		   pageSize: 10
		 },
		 //表格基本参数
		 options: {
		   stripe: false, // 是否为斑马纹 table
		   loading: true, // 是否添加表格loading加载动画
		   highlightCurrentRow: true, // 是否支持当前行高亮显示
		   mutiSelect: true, // 是否支持列表项选中功能
		   isFixed: true, //是否固定高
		   tableHeight: 340, //设置固定高高度(全屏减该数值获取table固定高度)
		 } // table 的参数
	  }
    },
	methods: {
		// 切换每页显示的数量
		handleSizeChange(pagination) {
		  this.pagination = pagination
		  this.params.limit = this.pagination.pageSize
		},
		// 切换页码
		handleIndexChange(pagination) {
		  this.pagination = pagination
		  this.params.page = this.pagination.pageIndex
		},
		// 选中行
		handleSelectionChange(val) {
		  let that = this;
		  let str = "";
		  let strname = "";
		  val.forEach(function(item) {
		    str += item.id + ",";
		  })
		  if (str.length > 0) {
		    that.multipleId = str.substr(0, str.length - 1);
		  } else {
		    that.multipleId = []
		  }
		},
	}
}
</script>
  1. 后台示例
  • 在mybatis-plus引入PageHelper
/**
 * Created by initsrc on 2020/02/26
 */
@Configuration
public class MybatisPlusConfig {

	//引入pageHelper
    @Bean
    ConfigurationCustomizer mybatisConfigurationCustomizer() {
        return new ConfigurationCustomizer() {
            @Override
            public void customize(org.apache.ibatis.session.Configuration configuration) {
                configuration.addInterceptor(new com.github.pagehelper.PageInterceptor());
            }
        };
    }
}
  • 封装PageHelper返回Vo
package com.initsrc.common.base;
import com.github.pagehelper.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@ApiModel(value = "PageResult")
@Data
public class PageResult<T> implements Serializable {
	
    @ApiModelProperty(value = "总条数")
    long total;

    @ApiModelProperty(value = "列表项")
    List<T> pageList;

    @ApiModelProperty(value = "单页显示数目")
    int size;

    @ApiModelProperty(value = "总页数")
    int pages;
    public PageResult(long total, int pages, int size, List<T> pageList) {
        this.total = total;
        this.pageList = pageList;
        this.size = size;
        this.pages = pages;
    }

    public static PageResult buildPageResult(Page page) {
        return new PageResult(page.getTotal(), page.getPages(), page.getPageSize(), page.getResult());
    }
}
  • 实现示例
@GetMapping("/noticeList")
public Result<PageResult<SysNoticeListVo>> pageData(@Validated SysNoticeQueryDto dto){
	Page<SysNoticeListVo> page = PageHelper.startPage(dto.page, dto.limit,"a.create_time "+dto.getSort());
	List<SysNoticeListVo> list = sysNoticeService.pageData(dto);
	return Result.success(PageResult.buildPageResult(page));
}

内置功能

  • 用户管理:实现增删改查、权限角色进行项目的登录与使用
  • 角色管理:实现增删改查、权限赋予、查询限制进行权限控制
  • 菜单管理:实现增删改查、赋予接口对应权限进行前后端接口管控
  • 部门管理:实现增删改查、权限控制源头,可定义查询分级
  • 通知公告:实现增删改查、系统通知公告
  • 字典管理:实现增删改查、自定义配置数组、文本、链接等系统参数
  • 操作日志:实现用户各种操作行为记录,系统追踪用户行为、异常等信息
  • 代码生成:前后端代码的生成(java、vue、xml、sql)
  • 系统接口:根据后端Swagger2生成的API管理系统
  • 终端控制:根据xterm和后端websocket+ssh技术进行控制liunx服务器
  • 服务监控:检测项目服务器、内存、CPU、硬盘等实时信息
  • 数据监控:检测数据库连接池状态、SQL分析等信息
  • 缓存监控:针对系统缓存信息的监控
  • 示例演示: 针对前端的各种表格呈现的DEMO

数据库概览

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wHYJRUwG-1623208607659)(…/img/1622636955(1)].jpg)

配置文件

  • application.properties 配置
spring.application.name=InitSrc
#banner
spring.banner.location=classpath:banner/banner.txt
spring.jmx.enabled=false
spring.profiles.active=dev

server.port=8520

#logging
logging.config=classpath:logback/logback-spring.xml

  • application-dev.yml 配置
spring:
  #redis配置
  redis:
    database: 0
    host: 127.0.0.1
    password:
    port: 6379
    jedis:
      pool:
        max-active: 50
        max-idle: 8
        min-idle: 0
  #数据源
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
    druid:
      stat-view-servlet:
        loginUsername: admin
        loginPassword: 123456
    dynamic:
      p6spy: true
      primary: master
      initial-size: 5
      max-active: 20
      min-idle: 5
      max-wait: 60000
      min-evictable-idle-time-millis: 30000
      max-evictable-idle-time-millis: 30000
      time-between-eviction-runs-millis: 0
      validation-query: select 1
      validation-query-timeout: -1
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      pool-prepared-statements: true
      max-open-prepared-statements: 100
      filters: stat,wall
      share-prepared-statements: true
      datasource:
        master:
          username: initsrc
          password: initsrc123
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://127.0.0.1:3306/initsrc?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
#        slave_1:
#          userName:
#          password:
#          driver-class-name: com.mysql.cj.jdbc.Driver
#          url: jdbc:mysql://?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
#          druid: #以下均为默认值
#            initial-size: 3
#            max-active: 8
#            min-idle: 2
#            max-wait: -1
#            min-evictable-idle-time-millis: 30000
#            max-evictable-idle-time-millis: 30000
#            time-between-eviction-runs-millis: 0
#            validation-query: select 1
#            validation-query-timeout: -1
#            test-on-borrow: false
#            test-on-return: false
#            test-while-idle: true
#            pool-prepared-statements: true
#            max-open-prepared-statements: 100
#            filters: stat,wall
#            share-prepared-statements: true
  #时间
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    serialization:
      write-dates-as-timestamps: true
#mybatis-plus
mybatis-plus:
  mapper-locations: classpath*:mappers/*/*.xml
  typeAliasesPackage: com.initsrc.*.*.entity,com.initsrc.*.entity,    #实体扫描,多个package用逗号或者分号分隔
  global-config:
    db-config:
      logic-delete-value: 0  #逻辑删除值
      logic-not-delete-value: 1  #逻辑不删除值
      id-type: assign_id
  configuration:
    map-underscore-to-camel-case: true  #驼峰命名
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#swagger
initsrc:
  swagger:
    show: true
    name: INITSRC
    url: https://www.initsrc.com:8520/doc.html
    email: [email protected]
    title: INITSRC 系统管理 API
    version: 1.0.0
    contact: MISTAKEAI
    license: license
    licenseUrl: https://www.initsrc.com
    description: 这是INITSRC的后台管理
    termsOfServiceUrl: https://www.initsrc.com
swagger:
  basic:
    enable: true
    username: initsrc
    password: initsrc123
# 防止XSS攻击
xss:
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes:
  # 匹配链接
  urlPatterns: /*
  • logback-spring.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">
    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <springProfile name="prod">
        <property name="LOG_PATH" value="logs/prod"/>
    </springProfile>

    <springProfile name="dev">
        <property name="LOG_PATH" value="logs/dev"/>
    </springProfile>

    <springProfile name="test">
        <property name="LOG_PATH" value="logs/test"/>
    </springProfile>

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>info</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>


    <!--输出到文件-->

    <!-- 时间滚动输出 level为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${LOG_PATH}/log_debug.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${LOG_PATH}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>20MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${LOG_PATH}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${LOG_PATH}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>50MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>180</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${LOG_PATH}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>10MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${LOG_PATH}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>180</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--开发环境:打印控制台-->
    <springProfile name="test">
        <logger name="com.initsrc" level="debug" additivity="false">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </logger>
    </springProfile>

    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <logger name="com.initsrc" level="debug" additivity="false">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </logger>
    </springProfile>

    <root level="info">
        <appender-ref ref="CONSOLE" />
<!--        <appender-ref ref="INFO_FILE" />-->
<!--        <appender-ref ref="WARN_FILE" />-->
<!--        <appender-ref ref="ERROR_FILE" />-->
    </root>

    <!--生产环境:输出到文件-->
    <springProfile name="prod">
        <logger name="com.initsrc" level="info" additivity="false">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </logger>
    </springProfile>
</configuration>

  • spy.properties 配置
//3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
// 自定义日志打印
//logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
//日志输出到控制台
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
// 使用日志系统记录 sql
//appender=com.p6spy.engine.spy.appender.Slf4JLogger
// 设置 p6spy driver 代理
deregisterdrivers=true
// 取消JDBC URL前缀
useprefix=true
// 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
// 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
// 实际驱动可多个
//driverlist=com.mysql.cj.jdbc.Driver
// 是否开启慢SQL记录
outagedetection=true
// 慢SQL记录标准 2 秒
outagedetectioninterval=2

项目部署

liunx服务器环境配置

  • 安装JDK
  1. 首先执行以下命令查看可安装的jdk版本:
yum -y list java*
  1. 选择自己需要的jdk版本进行安装,比如这里安装1.8,执行以下命令:
yum install -y java-1.8.0-openjdk-devel.x86_64
  1. 安装完成之后,查看安装的jdk版本,输入以下指令:
java -version
  1. 完成
  • 安装MYSQL
  1. 检测是否安装过mysql并且清理
rpm -qa | grep mysql
rpm -e --nodeps mysql 或 rm -rf xxx
  1. 下载YUM资源包并且安装
wget https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm
yum -y install mysql80-community-release-el7-2.noarch.rpm
yum -y install mysql-community-server
  1. 初始化
mysqld --initialize
  1. 赋予权限
chown -R mysql:mysql /var/lib/mysql/
  1. 启动
systemctl start mysqld.service //启动
systemctl status mysqld.service //查看mysql服务
  1. 查看临时密码
grep "password" /var/log/mysqld.log
  1. 登录mysql
mysql -uroot -p
  1. 设置密码
alter user 'root'@'localhost' identified by 'initsrc123';
  1. 设置开机启动
systemctl enable mysqld
systemctl daemon-reload
  1. 创建INITSRC数据源,并导入initsrc.sql文件
登录mysql后
create database initsrc;
source /XXX/XX 路径
  • 安装redis
  1. 下载redis包
wget http://download.redis.io/releases/redis-5.0.7.tar.gz
  1. 解压安装包
tar -zvxf redis-5.0.7.tar.gz
  1. 移动redis目录
mv redis-5.0.7 /usr/local/redis
  1. 编译redis
cd /usr/local/redis
make
  1. 安装redis
make PREFIX=/usr/local/redis install
  1. 启动redis
./bin/redis-server& ./redis.conf
  • 安装NGINX
  1. 安装编译工具及库文件
yum -y install make zlib zlib-devel gcc-c++ libtool  openssl openssl-devel
cd /usr/local/src/
wget http://downloads.sourceforge.net/project/pcre/pcre/8.35/pcre-8.35.tar.gz
tar zxvf pcre-8.35.tar.gz
cd pcre-8.35
./configure
make && make install
pcre-config --version
  1. 下载安装包
wget http://nginx.org/download/nginx-1.6.2.tar.gz
  1. 解压安装包并移动到/usr/local/nginx
tar zxvf nginx-1.6.2.tar.gz
mv nginx-1.6.2 /usr/local/nginx
  1. 编译安装
./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.35
make
make install
  1. 查看安装
/usr/local/webserver/nginx/sbin/nginx -v

windows服务器环境配置

  • 安装JDK
  1. 下载并且安装
通过网址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html进行下载对应版本的安装包,并且安装。(环境配置请自行查询配置)
  • 安装MYSQL
通过网址:https://dev.mysql.com/downloads/mysql/进行下载对应版本的安装包,并且安装。(环境配置请自行查询配置)
  • 安装REDIS
通过网址:https://github.com/MicrosoftArchive/redis/releases进行下载对应版本;
然后解压,cmd执行 redis-server.exe redis.windows.conf
  • 安装NGINX
通过网址:http://nginx.org/en/download.html进行下载对应版本;
然后解压

启动项目

  • 下载代码
项目地址:https://gitee.com/initsrc/initsrc.git
  • 修改application.yml
搭建好服务器环境,在修改application.yml配置里进行数据库数据切换、redis数据切换
  • 执行package命令
执行后,会在initsrc-base底下的target生成一个initsrc-base.jar包
  • 启动项目
启动命令 nohup java -jar  initsrc-base.jar  &
  • nginx配置
server
{
    listen 80;
    server_name 域名;
    index index.php index.html index.htm default.php default.htm default.html;
    root 项目前端路径;
    
    location /api/ {
		 rewrite ^/api/(.*)$ /$1 break;
		proxy_pass   http://127.0.0.1:指定端口/;
	}
}

前端页面展示![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/ebdbd2f14f7c726985a2ade05e058b49.png#pic_center在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41826583/article/details/117735548