介绍
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核心组件组成。
致力于协助初创科技企业建立技术储备、技术规范,让技术人员更加专注业务流,减少前期技术
搭建时间;此外,也帮助技术人员快速开发、减少重复工作。
应用环境
- JDK 1.8
- Apache Maven
- Servlet
- MYSQL8.0
- 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框架
- 介绍
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 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。
- SpringBoot优点
-
创建独立的Spring应用程序
-
嵌入的Tomcat,无需部署WAR文件
-
提供一个starter POMs来简化Maven配置
-
尽可能自动配置Spring
-
提供生产就绪型功能,如指标,健康检查和外部配置
-
绝对没有代码生成并且对XML也没有配置要求
Shiro + JWT
- Shiro 介绍
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
- JWT 介绍
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和
Mybaits-plus + Pagehelper
- Mybaits-plus 介绍
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
- 特性
-
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
-
损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
- PageHelper
PageHelper就是mybatis拦截器的一个应用,实现分页查询,支持常见的 12 种数据库的物理分页并支持多种分页方式。
Freemarker
- 介绍
Apache FreeMarker是一款开源的模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板使用FreeMarker Template Language(FTL)模板语言编写,这是一种简单的专用语言。模板用于展示数据,数据模型用于呈现什么数据。
- 特性
-
强大的模板语言
-
多用途且轻量
-
智能的国际化和本地化
-
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
- 介绍
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
Router
- 介绍
Vue Router 是 Vue.js (opens new window)官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
Vuex
- 介绍
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Element UI
- 介绍
Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库
- 特性
-
一致性 Consistency
与现实生活一致:与现实生活的流程、逻辑保持一致,遵循用户习惯的语言和概念;
在界面中一致:所有的元素和结构需保持一致,比如:设计样式、图标和文本、元素的位置等。 -
反馈 Feedback
控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;
页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。 -
效率 Efficiency
简化流程:设计简洁直观的操作流程;
清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;
帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。 -
可控 Controllability
用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;
结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。
Axios
- 介绍
axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,它本身具有以下特征:
- 从浏览器中创建 XMLHttpRequest
- 从 node.js 发出 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防止 CSRF/XSRF
前后端手册
后端获取用户信息方式
本项目为了方便用户在既定的方法里获取用户相关数据,例如用户ID,系统信息。我们运用了AOP方式进行参数注入,通过用户
在指定方法上添加@LoginUser,即可获取用户登录信息。
- 配置方式
新增注解
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进行记录操作记录;并且用户可以通过添加注解形式进行相对于操作的记录。
- 配置方式
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();
}
}
}
- 实现示例
在controller的方法上添加
@GetMapping("/page")
@LogAnnotation(operationType = LogOperateTypeEnum.SEARCH, operateContent = "查询数据")
public Result<PageResult<SysPermListVo>> pageData(){
}
后台事务注解
- 配置方式
通过注解的方式(也可以通过xml或者Java配置类的方式,不过没有使用注解的方式快)开启你的SpringBoot应用对事务的支持。
使用@EnableTransactionManagement注解(来自于上面引入的spring-tx包)
提示
@Transactional注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务。
@SpringBootApplication
@EnableTransactionManagement
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 实现示例
/**
* 测试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;
}
后台全局异常捕捉
- 权限异常捕捉
/**
* 描述:异常捕捉类
* 作者: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;
}
....
}
- 自定义业务异常
/**
* 描述:业务异常
* 作者: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来校验数据,并通过全局异常来捕获处理
- 实现示例
//在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;
}
- 异常捕捉
@ExceptionHandler(BindException.class)
public Map<String, Object> BindException(BindException e) {
//业务处理
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, Object> MethodArgumentNotValidException(MethodArgumentNotValidException e) {
//业务处理
}
后端数据查询权限
本项目采用AOP拦截方式进行对应权限的SQL的生成并通过参数形式在myabits的xml里进行拼接。
本项目的权限是根据组织架构进行数据权限控制。权限分别为:
- 全部权限
- 自定义部门权限
- 本部门及以下权限
- 本部门权限
- 配置方式
新增注解
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());
}
}
- 实现示例
在service需要设置权限的方法上添加,其中a代表的是表的别名。此后在mapper.xml里要控制权限的sql语句底部添加${params.dataScope}
@DataScope(deptAlias = "a")
<select id="xxx">
select * from xxx where 1=1
${params.dataScope}
</select>
前后端权限解析
- 后端采用Shiro的 @RequiresPermissions 注解进行权限控制
- 权限过滤核心
在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;
}
- 实现示例
在controller的方法上添加
@GetMapping("/page")
@RequiresPermissions("p:system:perm:page")
public Result<PageResult<SysPermListVo>> pageData(){
}
- 前端权限控制
- 前端获取权限格式
[
{
"color": "", //按钮颜色
"component": "", //VUE组件注册
"icon": "", //图标
"isCache": "", //是否缓存
"linkType": "", //链接类型
"name": "", //权限名称
"path": "", //前端路径
"perm": "", //后端 shiro 权限
"resource": "", //权限类型 0:菜单 1:按钮 2:表格按钮
"sort": 0 //排序
}
]
- 权限按钮转换工具
// 权限按钮过滤设置
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
},
- 实现示例
通过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插件。
- 前端示例
- 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>
- 后台示例
- 在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
- 首先执行以下命令查看可安装的jdk版本:
yum -y list java*
- 选择自己需要的jdk版本进行安装,比如这里安装1.8,执行以下命令:
yum install -y java-1.8.0-openjdk-devel.x86_64
- 安装完成之后,查看安装的jdk版本,输入以下指令:
java -version
- 完成
- 安装MYSQL
- 检测是否安装过mysql并且清理
rpm -qa | grep mysql
rpm -e --nodeps mysql 或 rm -rf xxx
- 下载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
- 初始化
mysqld --initialize
- 赋予权限
chown -R mysql:mysql /var/lib/mysql/
- 启动
systemctl start mysqld.service //启动
systemctl status mysqld.service //查看mysql服务
- 查看临时密码
grep "password" /var/log/mysqld.log
- 登录mysql
mysql -uroot -p
- 设置密码
alter user 'root'@'localhost' identified by 'initsrc123';
- 设置开机启动
systemctl enable mysqld
systemctl daemon-reload
- 创建INITSRC数据源,并导入initsrc.sql文件
登录mysql后
create database initsrc;
source /XXX/XX 路径
- 安装redis
- 下载redis包
wget http://download.redis.io/releases/redis-5.0.7.tar.gz
- 解压安装包
tar -zvxf redis-5.0.7.tar.gz
- 移动redis目录
mv redis-5.0.7 /usr/local/redis
- 编译redis
cd /usr/local/redis
make
- 安装redis
make PREFIX=/usr/local/redis install
- 启动redis
./bin/redis-server& ./redis.conf
- 安装NGINX
- 安装编译工具及库文件
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
- 下载安装包
wget http://nginx.org/download/nginx-1.6.2.tar.gz
- 解压安装包并移动到/usr/local/nginx
tar zxvf nginx-1.6.2.tar.gz
mv nginx-1.6.2 /usr/local/nginx
- 编译安装
./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
- 查看安装
/usr/local/webserver/nginx/sbin/nginx -v
windows服务器环境配置
- 安装JDK
- 下载并且安装
通过网址: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