An elegant and beautiful background management system, which makes development faster and allows you to pay more attention to business flow

introduce

INITSRC (Initiation)

INITSRC is an open source front-end and back-end management project for rapid development of individuals and small and medium-sized enterprises. Based on this project, users can develop and learn website management background, mall, OA, etc. The background of the project is implemented based on technologies such as Springboot+Mybaits-plus+Shiro+Jwt; the front end is implemented based on technologies such as Vue+Router+Vuex+Axios.

project demo

  • Project address: http://admin.initsrc.com
  • Account password: initsrc/a123456
  • Development documentation: docs.initsrc.com

contact us

QQ technology exchange group: 298264032

Remark

Undertake all kinds of software development, small programs, public accounts, APP. Interested contact WeChat: MISTAKEAI

Technology Introduction

The server is based on Springboot, Shiro + JWT, Mybaits-plus + Pagehelper, Freemarker core framework.

The front end is composed of core components based on Vue, Router, Vuex, Element UI, and Axios.

It is committed to assisting start-up technology companies to establish technical reserves and technical specifications, so that technicians can focus more on business flow and reduce the time for early technology
construction; in addition, it also helps technicians develop quickly and reduce repetitive work.

Application Environment

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

backend structure

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          // 其他模块

front-end structure

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

Back-end technology selection

technology Version illustrate
Spring Boot 2.3.0.RELEASE Container + MVC framework
Shiro 1.4.0 Authentication and Authorization Framework
JWT 3.3.0 Stateless Authentication Protocol
MyBatis-plus 3.3.2 ORM framework
pagehelper 5.1.10 paging plugin
Freemarker 2.3.28 code generation engine
Springfox-Swagger2 2.9.2 API document management
Redis 5.0 distributed cache
Druid 1.1.10 database connection pool
Lombok 1.18.6 Simplify Object Encapsulation Tool
Oshi-Core 3.9.1 Get application service information
P6spy 3.8.0 A Dynamic Monitoring Framework for Database Access Operations

Core Technology Introduction

Spring boot framework

  1. introduce

Spring Boot is a new framework provided by the Pivotal team. Spring Boot 1.0 was released in April 2014 and Spring Boot 2.0 was released in March 2018. It is a further package of spring, and its design purpose is to simplify the initial construction and development process of Spring applications. How to simplify it? It is to make it easier for us to use by encapsulating, abstracting, and providing default configurations.

SpringBoot is developed based on Spring. SpringBoot itself does not provide the core features and extended functions of the Spring framework, that is to say, it is not a solution to replace Spring, but a tool that is closely integrated with the Spring framework to improve the Spring developer experience.

There is a well-known saying about SpringBoot that convention is greater than configuration. Using Spring Boot can greatly simplify the development model. It integrates a large number of commonly used third-party library configurations. All common frameworks you want to integrate have corresponding component support, such as Redis, MongoDB, Jpa, Kafka, Hakira, etc. These third-party libraries in SpringBoot applications can be used almost out of the box with zero configuration. Most SpringBoot applications only require a very small amount of configuration code, allowing developers to focus more on business logic.

  1. Advantages of Spring Boot
  • Create standalone Spring applications

  • Embedded Tomcat, no need to deploy WAR file

  • Provides a starter POMs to simplify Maven configuration

  • Auto-configure Spring whenever possible

  • Provides production-ready features such as metrics, health checks, and external configuration

  • Absolutely no code generation and no configuration requirements for XML

Shiro + JWT

  1. Introduction

Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, password and session management. Using Shiro's easy-to-understand API, you can quickly and easily get any app, from the smallest mobile app to the largest web and enterprise app.

  1. Introduction to JWT

Json web token (JWT), is a JSON-based open standard implemented for passing claims between web application environments. The token is designed to be compact and secure, especially suitable for single sign-on (SSO) of distributed sites Scenes. JWT claims are generally used in identity providers and

Mybaits-plus + Pagehelper

  1. Introduction to Mybaits-plus

MyBatis-Plus (opens new window) (MP for short) is an enhancement tool for MyBatis (opens new window). On the basis of MyBatis, only enhancements are made without changes, and it was born to simplify development and improve efficiency.

  1. characteristic
  • No intrusion: only enhancement and no change, the introduction of it will not affect the existing project, as smooth as silk

  • Low loss: the basic CURD will be automatically injected when it is started, the performance is basically lossless, and the object-oriented operation is directly performed

  • Powerful CRUD operations: Built-in general Mapper and general Service, most of the CRUD operations on a single table can be realized with only a small amount of configuration, and there is a powerful condition constructor to meet various usage needs

  • Support Lambda form call: through Lambda expressions, it is convenient to write various query conditions, no need to worry about field typos

  • Supports automatic primary key generation: supports up to 4 primary key strategies (including a distributed unique ID generator - Sequence), which can be freely configured to perfectly solve the primary key problem

  • Support ActiveRecord mode: support ActiveRecord form call, the entity class only needs to inherit the Model class to perform powerful CRUD operations

  • Support custom global general operations: support global general method injection ( Write once, use anywhere )

  • Built-in code generator: use code or Maven plug-in to quickly generate Mapper, Model, Service, Controller layer code, support template engine, and more custom configurations are waiting for you to use

  • Built-in paging plug-in: Based on MyBatis physical paging, developers don't need to care about specific operations. After configuring the plug-in, writing paging is equivalent to ordinary List query

  • The paging plug-in supports multiple databases: supports MySQL, MariaDB, Oracle, DB2, H2, HSQL, SQLite, Postgre, SQLServer and other databases

  • Built-in performance analysis plug-in: It can output Sql statements and their execution time. It is recommended to enable this function during development and testing, which can quickly find out slow queries

  • Built-in global interception plug-in: Provides intelligent analysis and blocking of delete and update operations on the entire table, and can also customize interception rules to prevent misoperations

  1. PageHelper

PageHelper is an application of mybatis interceptor, which implements paging query, supports physical paging of 12 common databases and supports multiple paging methods.

Freemarker

  1. introduce

Apache FreeMarker is an open source template engine: a general-purpose tool for generating output text (HTML web pages, emails, configuration files, source code, etc.) based on templates and data to be changed. It is not intended for end users, but a java class library, a component that programmers can embed in the products they develop.

Templates are written using the FreeMarker Template Language (FTL) templating language, which is a simple specialized language. Templates are used to present data, and data models are used to present what data.

  1. characteristic
  • powerful templating language

  • versatile and lightweight

  • Intelligent internationalization and localization

  • XML processing capability

  • Common Data Model

Front-end technology selection

technology Version illustrate
Vue 2.6.11 A progressive framework for building user interfaces
vue-router 3.2.0 route manager
vuex 3.4.0 state management pattern
Element-ui 2.14.1 Front-end UI library
Axios 0.21.1 A promise-based HTTP library
view-apexcharts 1.6.0 Statistics View Library
Xterm 4.12.0 terminal emulator
@riophae/vue-treeselect 0.4.0 tree selector

Core Technology Introduction

Vue

  1. introduce

Vue (pronounced /vjuː/, similar to view) is a progressive framework for building user interfaces. Unlike other large frameworks, Vue is designed to be applied layer by layer from the bottom up. Vue's core library only focuses on the view layer, which is not only easy to use, but also easy to integrate with third-party libraries or existing projects. On the other hand, when combined with a modern toolchain and various supporting libraries, Vue is also fully capable of powering complex single-page applications.

Router

  1. introduce

Vue Router is the official route manager for Vue.js (opens new window). It is deeply integrated with the core of Vue.js, making it easy to build single-page applications. Included features are:

  • Nested routing/view tables
  • Modular, component-based routing configuration
  • Route parameters, queries, wildcards
  • View transition effect based on Vue.js transition system
  • Fine-grained navigation control
  • Links with auto-activated CSS classes
  • HTML5 history mode or hash mode, automatically degraded in IE9
  • custom scrollbar behavior

Vuex

  1. introduce

Vuex is a state management pattern developed specifically for Vue.js applications. It uses a centralized storage to manage the state of all components of the application, and uses corresponding rules to ensure that the state changes in a predictable manner.

Element UI

  1. introduce

Element, a Vue 2.0-based desktop component library for developers, designers and product managers

  1. characteristic
  • Consistency is
    consistent with real life: it is consistent with the process and logic of real life, and follows the language and concepts that users are used to; it is
    consistent in the interface: all elements and structures must be consistent, such as: design styles, icons and text, elements location etc.

  • Feedback Feedback
    Control Feedback: Allow users to clearly perceive their operations through interface style and interactive dynamic effects;
    Page Feedback: After the operation, the current status is clearly displayed through the changes of page elements.

  • Efficiency
    Simplifies the process: design a simple and intuitive operation process;
    clear and clear: the language is clear and expressive, allowing users to quickly understand and then make decisions;
    helping users to identify: the interface is simple and straightforward, allowing users to quickly identify rather than recall, reducing user memory burden.

  • Controllability
    User decision-making: According to the scene, the user can be given operation suggestions or safety tips, but it cannot replace the user to make decisions; the
    result is controllable: the user can freely perform operations, including canceling, rolling back, and terminating the current operation.

Axios

  1. introduce

Axios is a Promise-based HTTP client for browsers and nodejs, which itself has the following characteristics:

  • Create XMLHttpRequest from browser
  • Make http requests from node.js
  • Support Promise API
  • Intercept requests and responses
  • Transform request and response data
  • cancel request
  • Automatically convert JSON data
  • Client support to prevent CSRF/XSRF

Front and back end manual

How to obtain user information in the backend

This project is for the convenience of users to obtain user-related data in established methods, such as user ID and system information. We use the AOP method for parameter injection, and the
user's login information can be obtained by adding @LoginUser to the specified method.

  1. configuration method

add comment

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

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

Annotation implementation

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

Backend logging method

The operation log of this project uses AOP to record operation records; and users can record relative operations by adding annotations.

  1. configuration method
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. Implementation example
    Add on the method of controller
@GetMapping("/page")
@LogAnnotation(operationType = LogOperateTypeEnum.SEARCH, operateContent = "查询数据")
public Result<PageResult<SysPermListVo>> pageData(){
}

Notes on background transactions

  1. Configuration method
    Enable your SpringBoot application to support transactions through annotations (you can also configure classes through xml or Java, but it is not as fast as using annotations).
    Use the @EnableTransactionManagement annotation (from the spring-tx package introduced above)

Tip
@Transactional annotation can only be applied to methods with public visibility, and can be applied to interface definitions and interface methods. The method will override the transaction declared above the class.

@SpringBootApplication
@EnableTransactionManagement
public class Application {

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

}
  1. Implementation example
/**
 * 测试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;
}

Background global exception capture

  1. Permission exception capture
/**
 * 描述:异常捕捉类
 * 作者: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. custom business exception
/**
 * 描述:业务异常
 * 作者: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;
}

Background parameter verification

@Validated is used in the background to verify data, and global exceptions are used to capture and process

  1. Implementation example
//在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. exception capture
@ExceptionHandler(BindException.class)
    public Map<String, Object> BindException(BindException e) {
        //业务处理
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, Object> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
	//业务处理
}

Backend data query permission

This project uses the AOP interception method to generate the SQL corresponding to the authority and splicing in the xml of myabits through the form of parameters.
The authority of this project is to control the data authority according to the organizational structure. The permissions are:

  • full permissions
  • Custom Department Permissions
  • This department and the following authority
  • Authority of this department
  1. configuration method

add comment

package com.initsrc.common.annotation;

import java.lang.annotation.*;

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

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

Annotation implementation

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. Implementation example

Add it to the method that service needs to set permissions, where a represents the alias of the table. After that, add ${params.dataScope} at the bottom of the sql statement to control permissions in mapper.xml

@DataScope(deptAlias = "a")

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

Analysis of front-end and back-end permissions

  • The backend uses Shiro's @RequiresPermissions annotation for permission control
  1. permission filtering core

Implemented in Shiro's custom 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. Implementation example

Add to the method of the controller

@GetMapping("/page")
@RequiresPermissions("p:system:perm:page")
public Result<PageResult<SysPermListVo>> pageData(){
}
  • Front-end permission control
  1. Front-end access permission format
[
	{
		"color": "",  //按钮颜色
		"component": "", //VUE组件注册
		"icon": "", //图标
		"isCache": "", //是否缓存
		"linkType": "", //链接类型
		"name": "", //权限名称
		"path": "", //前端路径
		"perm": "", //后端 shiro 权限
		"resource": "", //权限类型 0:菜单 1:按钮 2:表格按钮
		"sort": 0 //排序
	}
]
  1. Permission button conversion tool
// 权限按钮过滤设置
  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. Implementation example
    Obtain the permission array object of the current menu through 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)
 }

By entering the mounted page, execute the permission conversion tool to obtain the corresponding button permission.

Tip: Why not do it globally, because the permissions of each page may have some special processing.

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
}

Front-end and back-end pagination analysis

The front-end paging uses the table component of Eelement and the is-table encapsulated by the Pagination component. The backend uses the Pagehelper plugin.

  1. front end example
  • table encapsulation code
  /**
  * 作者:大神很烦恼
  * 邮箱:[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>
  • used in the component
<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. background example
  • Introduce PageHelper in mybatis-plus
/**
 * 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());
            }
        };
    }
}
  • Encapsulate PageHelper to return 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());
    }
}
  • Implementation example
@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));
}

built-in function

  • User management: Realize adding, deleting, modifying and checking, permission roles to log in and use projects
  • Role management: Realize adding, deleting, modifying, checking, granting permissions, and querying restrictions for permission control
  • Menu management: Realize adding, deleting, modifying and checking, granting corresponding permissions to the interface to control front-end and back-end interfaces
  • Department management: realize adding, deleting, modifying and checking, authority control source, and can define query classification
  • Notification announcement: Realize adding, deleting, modifying and checking, system notification announcement
  • Dictionary management: realize addition, deletion, modification and query, custom configuration array, text, link and other system parameters
  • Operation log: Realize the recording of various user operation behaviors, and the system tracks user behaviors, abnormalities and other information
  • Code generation: front-end and back-end code generation (java, vue, xml, sql)
  • System interface: API management system generated according to the backend Swagger2
  • Terminal control: control liunx server based on xterm and backend websocket+ssh technology
  • Service monitoring: detect real-time information such as project server, memory, CPU, hard disk, etc.
  • Data monitoring: Detect database connection pool status, SQL analysis and other information
  • Cache monitoring: monitoring of system cache information
  • Example demonstration: DEMO presented for various tables in the front end

Database overview

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-wHYJRUwG-1623208607659)(…/img/1622636955(1)].jpg)

configuration file

  • application.properties configuration
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 configuration
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 configuration
<?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 configuration
//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

project deployment

Liunx server environment configuration

  • Install JDK
  1. First execute the following command to check the version of jdk that can be installed:
yum -y list java*
  1. Select the jdk version you need to install, for example, to install 1.8 here, execute the following command:
yum install -y java-1.8.0-openjdk-devel.x86_64
  1. After the installation is complete, check the installed jdk version and enter the following command:
java -version
  1. Finish
  • Install MySQL
  1. Detect whether mysql has been installed and clean up
rpm -qa | grep mysql
rpm -e --nodeps mysql 或 rm -rf xxx
  1. Download the YUM resource package and install it
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. initialization
mysqld --initialize
  1. grant permission
chown -R mysql:mysql /var/lib/mysql/
  1. start up
systemctl start mysqld.service //启动
systemctl status mysqld.service //查看mysql服务
  1. view temporary password
grep "password" /var/log/mysqld.log
  1. log in to mysql
mysql -uroot -p
  1. set password
alter user 'root'@'localhost' identified by 'initsrc123';
  1. Set boot
systemctl enable mysqld
systemctl daemon-reload
  1. Create the INITSRC data source and import the initsrc.sql file
登录mysql后
create database initsrc;
source /XXX/XX 路径
  • install redis
  1. Download the redis package
wget http://download.redis.io/releases/redis-5.0.7.tar.gz
  1. Unzip the installation package
tar -zvxf redis-5.0.7.tar.gz
  1. Move the redis directory
mv redis-5.0.7 /usr/local/redis
  1. compile redis
cd /usr/local/redis
make
  1. install redis
make PREFIX=/usr/local/redis install
  1. start redis
./bin/redis-server& ./redis.conf
  • Install NGINX
  1. Install compilation tools and library files
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. Download the installation package
wget http://nginx.org/download/nginx-1.6.2.tar.gz
  1. Unzip the installation package and move to /usr/local/nginx
tar zxvf nginx-1.6.2.tar.gz
mv nginx-1.6.2 /usr/local/nginx
  1. Compile and install
./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. view installation
/usr/local/webserver/nginx/sbin/nginx -v

windows server environment configuration

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

Startup project

  • download code
项目地址:https://gitee.com/initsrc/initsrc.git
  • Modify application.yml
搭建好服务器环境,在修改application.yml配置里进行数据库数据切换、redis数据切换
  • Execute the package command
执行后,会在initsrc-base底下的target生成一个initsrc-base.jar包
  • Startup project
启动命令 nohup java -jar  initsrc-base.jar  &
  • nginx configuration
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:指定端口/;
	}
}

Front-end page display! [Insert picture description here](https://img-blog.csdnimg.cn/img_convert/ebdbd2f14f7c726985a2ade05e058b49.png#pic_centerinsert image description hereinsert image description here

insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here

Guess you like

Origin blog.csdn.net/weixin_41826583/article/details/117735548