# 常见问题

# 如何新增系统图标

如果你没有在本项目 Icon (opens new window) 中找到需要的图标,可以到 iconfont.cn (opens new window)上选择并生成自己的业务图标库,再进行使用。或者其它svg 图标网站,下载svg 并放到文件夹之中就可以了。

下载完成之后将下载好的.svg 文件放入 @/icons/svg 文件夹下之后就会自动导入。

使用方式

<svg-icon icon-class="password" /> // icon-class 为 icon 的名字

提示

菜单图标会自动引入@/icons/svg,放入此文件夹中图标就可以选择了

# 如何不登录直接访问

SecurityConfig 中设置httpSecurity 配置匿名访问

// 使用 permitAll() 方法所有人都能访问,包括带上 token 访问
.antMatchers("/admins/**").permitAll()

// 使用 anonymous() 所有人都能访问,但是带上 token 访问后会报错
.antMatchers("/admins/**").anonymous()

提示

匿名访问方法@PreAuthorize权限注解也需要去掉。

前端不登录如何直接访问

如果是前端页面可以在src/permission.js配置whiteList属性白名单即可。

# 如何更换项目包路径

1、更换目录名称

├── xxxxx
│       └── xxxxx-admin
│       └── xxxxx-common
│       └── xxxxx-framework
│       └── xxxxx-generator
│       └── xxxxx-quartz
│       └── xxxxx-system
│       └── pom.xml

2、更换顶级目录中的pom.xml

<modules>
	<module>xxxxx-admin</module>
	<module>xxxxx-framework</module>
	<module>xxxxx-system</module>
	<module>xxxxx-quartz</module>
	<module>xxxxx-generator</module>
	<module>xxxxx-common</module>
</modules>

3、更换项目所有包名称com.aidex.xxx换成com.xxxxx.xxx

提示

DataScopeAspect,DataSourceAspect,LogAspect 这三个类@Pointcut注解上面的包路径也需要替换com.xxxxx

CaptchaConfig 这个类验证码文本生成器参数KAPTCHA_TEXTPRODUCER_IMPL的包路径也需要替换com.xxxxx

ApplicationConfig 这个类@MapperScan注解上面的包路径也需要替换com.xxxxx

4、更换application.yml指定要扫描的Mapper类的包的路径typeAliasesPackage包路径名称替换com.xxxxx

# MyBatis
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.你的包名.**.domain

5、更换mapper文件的namespace包路径

aidex-system/resources/mapper/system/* 
aidex-quartz/resources/mapper/quartz/* 
aidex-generator/resources/mapper/generator/*

xml包路径名称替换com.xxxxx

6、更换pom文件内容

提示

以下pom.xml文件中包含aidex的关键字替换成xxxxx

├── xxxxx
│       └── xxxxx-admin      pom.xml
│       └── xxxxx-common     pom.xml
│       └── xxxxx-framework  pom.xml
│       └── xxxxx-generator  pom.xml
│       └── xxxxx-quartz     pom.xml
│       └── xxxxx-system     pom.xml
│       └── pom.xml

7、更换日志路径

  • 更换application.yml文件logging属性为com.xxxxx: debug
  • 更换logback.xml文件为com.xxxxx

8、启动项目验证

提示

到此步骤如能正常启动,表示更换完成。剩余的小细节可以自行调整。

# 业务模块访问出现404

1、单应用检查

  • 确认此用户是否已经配置菜单
  • 确认此角色是否已经配置菜单权限
  • 确认此菜单url是否和后台地址一致

如参数管理,后台地址配置@RequestMapping("/system/config")对应参数管理url为/system/config

2、多模块检查(多了几个步骤)

  • pom.xml 引入了业务子系统
  • aidex-admin 添加业务子模块的依赖
  • aidex-xxxxx 新增业务模块pom检查配置是否正确

PS:IDEA可能存在缓存,需要清理下缓存在编译。

提示

如果业务模块和项目的包名不一致,需要在启动类上指定扫描包路径,如 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }, scanBasePackages = { "com.aidex.", "com.test." }) 或者加上@ComponentScan({ "com.aidex.", "com.test." })

# 如何使用多数据源

对于只有两个数据源直接配置slave加入注解即可。

1、在 resources 目录下修改application-druid.yml

# 从库数据源
slave:
    # 开启从库
    enabled: true
    url: 数据源
    username: 用户名
    password: 密码

2、 在Service实现中添加DataSource注解

@DataSource(value = DataSourceType.SLAVE)
public List<User> selectUserList()
{
    return mapper.selectUserList();
}

# 如何更换主题皮肤

默认的主题都是深色主题,如果需要其他主题可以做如下配置。

1、点击顶部最右侧个人中心头像,选择布局设置,选择主题风格设置。(局部设置)

2、在aidex-ui\src\settings.js,设置侧边栏主题sideThemetheme-xxxx。(全局设置)

# 如何使用横向菜单

默认的导航菜单都是在左侧,如果需要横向导航菜单可以做如下配置。

1、点击顶部最右侧个人中心头像,选择布局设置,开启TopNav。(局部设置)

2、在aidex-ui\src\settings.js,设置是否显示顶部导航topNavtrue。(全局设置)

# 系统接口访问出现401

在测试系统接口中可能存在一些接口用到用户信息或权限验证,此时需要添加全局的token参数。

token是在登录成功后返回的,可以在浏览器通过F12查看Network中的请求地址,对应参数Authorization。复制截图内容到swagger全局Authorization属性value参数中,点击Authorize,以后每次访问接口会携带此token信息。

# 如何更换后端请求地址

vue.config.js中,修改target值为对应的的后端接口地址。

devServer: {
  ...,
  proxy: {
    [process.env.VUE_APP_BASE_API]: {
      target: `http://localhost:8080`,
      ...
    }
  },
  ...
},

# 如何启动项目https协议

通常情况下,在启动本地项目时,默认都是http协议,但是有时候测试网站要求我们的协议是https,那么可以配置vue.config.js中的devServer,让其在启动项目的时候,默认是https协议。

module.exports = {
    ......
	devServer: {
	  https: true,
	  ......
	},
}

# 如何获取用户登录信息

第一种方法

// 获取当前的用户名称
String username = SecurityUtils.getUsername();

缓存获取当前用户信息

@Autowired
private TokenService tokenService;
	
LoginUser loginUser = tokenService.getLoginUser();
// 获取当前的用户名称
String username = loginUser.getUsername();

vue中获取当前用户信息

const username = this.$store.state.user.name;

# 提示您没有数据的权限

# 如何创建新的菜单页签

#如何支持多类型数据

对于某些特殊需要支持不同数据库,参考以下支持oracle mysql配置

<!--oracle驱动-->
<dependency>
	<groupId>com.oracle</groupId>
	<artifactId>ojdbc6</artifactId>
	<version>11.2.0.3</version>
</dependency>
# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: password
            # 从库数据源
            slave:
                # 从数据源开关/默认关闭
                enabled: true
                url: jdbc:oracle:thin:@127.0.0.1:1521:oracle
                username: root
                password: password

对于不同数据源造成的驱动问题,可以删除driverClassName。会自动识别驱动 如需要对不同数据源分页需要操作application.yml中的pagehelper配置 删除helperDialect: mysql 会自动识别数据源 新增autoRuntimeDialect=true 表示运行时获取数据源

# 如何降低mysql驱动版本

1、在pom.xml中properties新增节点如:

<mysql.version>6.0.6</mysql.version>

2、单应用可以不添加,多模块需要在dependencyManagement声明依赖

<!-- Mysql驱动包 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>${mysql.version}</version>
</dependency>

注意: 如果是6以下的版本需要修改application-druid.yml中driverClassName com.mysql.jdbc.Driver 是 mysql-connector-java 5中的 com.mysql.cj.jdbc.Driver 是 mysql-connector-java 6中的

# 如何配置tomcat访问日志

1、修改application.yml中的server开发环境配置

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为80
  port: 80
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # 存放Tomcat的日志目录
    basedir: D:/tomcat
    accesslog: 
        # 开启日志记录
        enabled: true
        # 访问日志存放目录
        directory: logs
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30

2、重启项目后,在D:/tomcat/logs目录就可以看到服务器访问日志了

# 如何配置项目访问根路径

目前项目后台访问默认路径是:http://localhost:80,如果需要自定义项目名或端口可以修改配置文件src/main/resources/application.yml

server:
  port: 8080
  servlet:
     context-path: /aidex

此配置后访问的默认路径是:http://localhost:8080/aidex

# Swagger的启用和禁用

可通过application.yml中的swagger.enable控制。为true时表示启用,为false时表示禁用。

记得关闭

为了系统安全,通常生产环境不建议开启swagger。

# 如何汉化系统接口Swagger

想必很多小伙伴都曾经使用过Swagger,但是打开UI界面是纯英文的界面并不太友好,作为国人还是习惯中文界面。

1、找到m2/repository/io/springfox/springfox-swagger-ui/x.x.x/springfox-swagger-ui-x.x.x.jar

2、修改对应springfox-swagger-ui-x.x.x.jar包内resources目录下swagger-ui.html,添加如下JS代码

<!-- 选择中文版 -->
<script src='webjars/springfox-swagger-ui/lang/translator.js' type='text/javascript'></script>
<script src='webjars/springfox-swagger-ui/lang/zh-cn.js' type='text/javascript'></script>

3、本地修改结束后,在覆盖压缩包文件重启就实现汉化了

# Swagger接口出现转换错误

{
  "msg": "Failed to convert property value of type 'java.lang.String' to required type 'java.util.Map' for property 'params'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map' for property 'params': no matching editors or conversion strategy found",
  "code": 500
}

一般在Swagger页面执行查询接口时出现。params是BaseEntity.java的属性,请求的时候把默认的值{}置空就行了。

如果不想看到他,BaseEntity.java中的params参数加上@ApiModelProperty(hidden = true)

/** 请求参数 */
@ApiModelProperty(hidden = true)
private Map<String, Object> params;

# 如何在html页面格式化日期

Thymeleaf主要使用org.thymeleaf.expression.Dates这个类来处理日期,在模板中使用"#dates"来表示这个对象。

1、格式化日期

  • [[${#dates.format(date)}]] 或 th:text="${#dates.format(date)}
  • [[${#dates.formatISO(date)}]] 或 th:text="${#dates.formatISO(date)} -[[${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}]] 或 th:text="${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}

2、获取日期字段

  • 获取当前的年份:[[${#dates.year(date)}]]
  • 获取当前的月份:[[${#dates.month(date)}]]
  • 获取当月的天数:[[${#dates.day(date)}]]
  • 获取当前的小时:[[${#dates.hour(date)}]]
  • 获取当前的分钟:[[${#dates.minute(date)}]]
  • 获取当前的秒数:[[${#dates.second(date)}]]
  • 获取当前的毫秒:[[${#dates.millisecond(date)}]]
  • 获取当前的月份名称:[[${#dates.monthName(date)}]]
  • 获取当前是星期几:[[${#dates.dayOfWeek(date)-1}]]

# 如何在表格中实现图片预览

对于某些图片需要在表格中显示,可以使用imageView方法

// 在columns中格式化对应相关的列属性
{
	field: 'avatar',
	title: '用户头像',
	formatter: function(value, row, index) {
		return $.table.imageView(value, '/profile/avatar');
	}
},

多图片预览可以自己实现,示例。

// 传入value 图片地址数组
previewImg: function(value) {
    var data = [];
    for (var key of value) {
        var json = {};
        json.src = key;
        data.push(json);
    }
    layer.photos({
        photos: {
            "data": data
        },
        anim: 6 // 0-6的选择,指定弹出图片动画类型,默认随机
    });
},

# 如何去掉页脚及左侧菜单栏

1、去除页脚修改style.css,同时删除index.html元素

#content-main {
    height: calc(100% - 91px);
    overflow: hidden;
}

<div class="footer">
    <div class="pull-right">© [[${copyrightYear}]] aidex Copyright </div>
</div>

2、去左侧菜单栏(收起时隐藏左侧菜单)修改style.css

body.fixed-sidebar.mini-navbar #page-wrapper {
    margin: 0 0 0 0px;
}

body.body-small.fixed-sidebar.mini-navbar #page-wrapper {
    margin: 0 0 0 0px;
}

3、去左侧菜单栏(收起时隐藏左侧菜单)修改index.js

function() {
    if ($(this).width() < 769) {
        $('body').addClass('mini-navbar');
        $('.navbar-static-side').fadeIn(); // 换成 $('.navbar-static-side').hide();
        $(".sidebar-collapse .logo").addClass("hide");
    }
});

function SmoothlyMenu() {
    if (!$('body').hasClass('mini-navbar')) {
    	$(".navbar-static-side").show();  // 添加显示这一行
        $('#side-menu').hide();
        $(".sidebar-collapse .logo").removeClass("hide");
        setTimeout(function() {
            $('#side-menu').fadeIn(500);
        },
        100);
    } else if ($('body').hasClass('fixed-sidebar')) {
    	$(".navbar-static-side").hide();  // 添加隐藏这一行
        $('#side-menu').hide();
        $(".sidebar-collapse .logo").addClass("hide");
        setTimeout(function() {
            $('#side-menu').fadeIn(500);
        },
        300);
    } else {
        $('#side-menu').removeAttr('style');
    }
}

4、隐藏左侧菜单,需要添加.canvas-menu到body元素

<body class = "canvas-menu"> 

# 登录页如何开启注册用户功能

在菜单参数设置修改参数键名sys.account.registerUser设置true即可。默认为false关闭。

# 如何限制账户只能一个人登录

在application.yml设置maxSession为1即可。

# Shiro
shiro:
  session:
    # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
    maxSession: 1
    # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
    kickoutAfter: false

# 登录页面如何不显示验证码

在application.yml设置captchaEnabled为false即可

# Shiro
shiro:
  user:
    # 验证码开关
    captchaEnabled: false

# 如何Excel导出子对象多个字段

// 单个字段导出
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT)
private Dept dept;

// 多个字段导出
@Excels({
    @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
    @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
})
private Dept dept;

# 更多操作字符串参数读取问题

事件中需要传递字符串参数,可以参考resetPwd传递方式。

onclick=resetPwd(" + row.userId + ',' + "'" + row.userName + "'" + ")
formatter: function(value, row, index) {
	var actions = [];
	actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.editTab(\'' + row.userId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
	actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.userId + '\')"><i class="fa fa-remove"></i>删除</a> ');
	var more = [];
	more.push("<a class='btn btn-default btn-xs " + resetPwdFlag + "' href='javascript:void(0)' onclick=resetPwd(" + row.userId + ',' + "'" + row.userName + "'" + ")><i class='fa fa-key'></i>重置密码</a> ");
	more.push("<a class='btn btn-default btn-xs " + editFlag + "' href='javascript:void(0)' onclick='authRole(" + row.userId + ")'><i class='fa fa-check-square-o'></i>分配角色</a>");
	actions.push('<a tabindex="0" class="btn btn-info btn-xs" role="button" data-container="body" data-placement="left" data-toggle="popover" data-html="true" data-trigger="hover" data-content="' + more.join('') + '"><i class="fa fa-chevron-circle-right"></i>更多操作</a>');
	return actions.join('');
}

# 单元格内容过长显示处理方法

1、使用系统自带的方法格式化处理

{
	field: 'remark',
	title: '备注',
	align: 'center',
	formatter: function(value, row, index) {
		return $.table.tooltip(value);
	}
},

2、添加css控制

.select-table table {
    table-layout:fixed;
}

.select-table .table td {
	/* 超出部分隐藏 */
	overflow:hidden;
	/* 超出部分显示省略号 */
    text-overflow:ellipsis;
    /*规定段落中的文本不进行换行 */
    white-space:nowrap;
    /* 配合宽度来使用 */
	height:40px;
}

# 表格禁用某列复选框选择方法

条件成立禁用checkbox返回(disabled : true)即可。

{
	checkbox: true,
	formatter: function (value, row, index) {
		if($.common.equals("ry", row.loginName)){
			return { disabled : true}
		} else {
			return { disabled : false}
		}
	}
},

# 表格默认勾选某列复选框方法

条件成立禁用checkbox返回(disabled : true)即可。

{
	checkbox: true,
	formatter: function (value, row, index) {
		if($.common.equals("ry", row.loginName)){
			return { checked : true}
		} else {
			return { checked : false}
		}
	}
},

提示

如果默认勾选,并且配置了 rememberSelected: true, 需要特殊处理下。参考demo

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
	<th:block th:include="include :: header('翻页记住选择')" />
</head>
<body class="gray-bg">
     <div class="container-div">
     	<div class="btn-group-sm" id="toolbar" role="group">
	        <a class="btn btn-success" onclick="checkItem()">
	            <i class="fa fa-check"></i> 选中项
	        </a>
        </div>
		<div class="row">
			<div class="col-sm-12 select-table table-striped">
				<table id="bootstrap-table"></table>
			</div>
		</div>
	</div>
    <div th:include="include :: footer"></div>
    <script th:inline="javascript">
        var prefix = ctx + "demo/table";
        var datas = [[${@dict.getType('sys_normal_disable')}]];

        $(function() {
            var options = {
                uniqueId: "userCode",
                url: prefix + "/list",
		        rememberSelected: true,
                columns: [{
                	field: 'state',
		            checkbox: true,
		            formatter: function(value, row, index) {
		            	if($.inArray(row.userCode, table.rememberSelectedIds[table.options.id]) !== -1 || row.userCode == 1000001 || row.userCode == 1000002){
		            		if($.inArray(row.userCode, uncheckUserCode) !== -1)
		            		{
		            			return { checked : false };
		            		}
		            		var selectedRows = table.rememberSelecteds[table.options.id];
		            		func = $.inArray('check', ['check', 'check-all']) > -1 ? 'union' : 'difference';
		            		if($.common.isNotEmpty(selectedRows)) {
		            			table.rememberSelecteds[table.options.id] = _[func](selectedRows, row);
	            			} else {
	            				table.rememberSelecteds[table.options.id] = _[func]([], row);
	            			}
		            		return { checked : true };
		            	}
		            	return { checked : false };
		        	}
		        },
				{
					field : 'userId', 
					title : '用户ID'
				},
				{
					field : 'userCode', 
					title : '用户编号'
				},
				{
					field : 'userName', 
					title : '用户姓名'
				},
				{
					field : 'userPhone', 
					title : '用户手机'
				},
				{
					field : 'userEmail', 
					title : '用户邮箱'
				},
				{
				    field : 'userBalance',
				    title : '用户余额'
				},
				{
                    field: 'status',
                    title: '用户状态',
                    align: 'center',
                    formatter: function(value, row, index) {
                    	return $.table.selectDictLabel(datas, value);
                    }
                },
		        {
		            title: '操作',
		            align: 'center',
		            formatter: function(value, row, index) {
		            	var actions = [];
		            	actions.push('<a class="btn btn-success btn-xs" href="#"><i class="fa fa-edit"></i>编辑</a> ');
                        actions.push('<a class="btn btn-danger btn-xs" href="#"><i class="fa fa-remove"></i>删除</a>');
						return actions.join('');
		            }
		        }]
            };
            $.table.init(options);
        });
        
        
        var uncheckUserCode = [];
    	$("#bootstrap-table").on("uncheck.bs.table uncheck-all.bs.table", function (e, rows) {
    		if(rows.length > 0) {
    			for (var index in rows) {
    				uncheckUserCode.unshift(rows[index].userCode);
   		        }
    		} else {
    			uncheckUserCode.unshift(rows.userCode);
    		}
    	});
        
    	$("#bootstrap-table").on("check.bs.table check-all.bs.table", function (e, rows) {
    		if(rows.length > 0) {
    			for (var index in rows) {
    				deleteItem(rows[index].userCode);
   		        }
    		} else {
    			deleteItem(rows.userCode);
    		}
    	});
    	
    	function deleteItem(item) {
    	    for (var key in uncheckUserCode) {
    	        if (uncheckUserCode[key] === item) {
    	        	uncheckUserCode.splice(key, 1)
    	        }
    	    }
    	}
        
        // 选中数据
        function checkItem(){
        	// var arrays = $.table.selectColumns("userId");
        	var arrays = $.table.selectColumns("userCode");
        	alert(arrays);
        }
    </script>
</body>
</html>

# 页面如何一次初始化多个表格

在options中添加id参数,如果有按钮组也需要添加toolbar。

// 表格1
var options = {
	id: "bootstrap-table1",
    toolbar: "toolbar1",
	// 省略 ....
};
$.table.init(options);

// 表格2
var options = {
	id: "bootstrap-table2",
    toolbar: "toolbar2",
	// 省略 ....
};
$.table.init(options);

# 表格底部合计列拖动显示问题

在options中添加onLoadSuccess参数。

onLoadSuccess: onLoadSuccess,

// 监听表体fixed-table-body滚动事件,赋值给表尾fixed-table-footer
function onLoadSuccess() {
	$(".fixed-table-body").on("scroll",function(){
		var sl=this.scrollLeft;
		$(this).next()[0].scrollLeft = sl;
	})
}

# 表格操作列传递行数据的对象

传递JSON字符串

actions.push('<a class="btn btn-success btn-xs href="javascript:void(0)" onclick="edit(' + JSON.stringify(row).replace(/"/g, '&quot;') + ')"><i class="fa fa-edit"></i>编辑</a> ');

传递JSON对象

actions.push("<a class='btn btn-success btn-xs href='javascript:void(0)' onclick='edit(" + JSON.stringify(row) + ")'><i class='fa fa-edit'></i>编辑</a> ");

获取xxxx字段

function edit(row) {
    alert(row.xxxx);
}

# 日期控件初始化时间并格式化

使用thymeleaf在页面直接获取当前时间并格式化输出

<input type="text" th:value="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}" />
<a th:text="${#dates.format(new java.util.Date().getTime(), 'yyyy-MM-dd HH:mm:ss')}">time</a>

# 如何调整首页左侧菜单栏宽度

调整style.css对应样式宽度,例如宽度200修改成250

body.fixed-sidebar .navbar-static-side, body.canvas-menu .navbar-static-side {
    width: 250px;
}
nav .logo {
	width: 250px;
}
#page-wrapper {
    margin: 0 0 0 250px;
}

# 如何默认显示表格卡片视图

在options中添加 mobileResponsive cardView 参数

mobileResponsive: false,
cardView: true,

# 编辑和删除操作按钮不可用

这种情况一般是因为第一列不是唯一键或formatter序号造成的。解决方案如下,指定唯一列属性 配合删除/修改使用 未指定则使用表格行首列

在options中添加 uniqueId参数,userId修改成你表的唯一列字段。

uniqueId: 'userId',

# Tomcat部署多个War包项目异常

default-domain的值不一样就可以了 在application.yml里面配置上

spring:
  jmx:
    default-domain: applicationname

# 部署多个项目Ehcache缓存异常

同一服务器部署多个项目,有可能会导致shiro-activeSessionCache缓存冲突。

net.sf.ehcache.ObjectExistsException: Cache shiro-activeSessionCache already exists

可以在ShiroConfig设置不同名称区分一下就可以了。

public OnlineSessionDAO sessionDAO()
{
        OnlineSessionDAO sessionDAO = new OnlineSessionDAO();
        sessionDAO.setActiveSessionsCacheName("缓存名字");
        return sessionDAO;
}

# Tomcat临时目录tmp抛错误异常

首先,我们应该知道,对于http POST请求来说,它需要使用这个临时目录来存储post数据。 其次,因为该目录是挂在到/temp目录下的临时文件,那么对于一些OS系统,像centOS将经常删除这个临时目录,所有导致该目录不存在了

解决方案

1.在application.yml文件中设置multipart location ,并重启项目

spring:
  http:
    multipart:
      location: /data/upload_tmp

2.在application.yml文件中设置

server
  tomcat:
     basedir: /tmp/tomcat

3.在配置文件添加bean

@Bean
public MultipartConfigElement multipartConfigElement() {
  MultipartConfigFactory factory = new MultipartConfigFactory();
  factory.setLocation("/tmp/tomcat");
  return factory.createMultipartConfig();
}

4.添加启动参数-java.tmp.dir=/path/to/application/temp/,并重启。

# 如何部署配置支持https访问

Nginx 配置为例,完整流程如下

申请下载ssl证书 证书有很多种,申请成功后会得到一个压缩包,里面有2个证书

1、安装OpenSSL yum -y install openssl openssl-devel

2、运行添加ssl模块 ./configure --prefix=/usr/local/nginx --with-http_ssl_module

3、配置完成后,运行命令make

4、然后将刚刚编译好的nginx覆盖掉原有的nginx(这个时候nginx要停止状态) cp objs/nginx /usr/local/nginx/sbin/

5、复制crt证书文件和key私钥文件到Nginx服务器/usr/local/nginx/conf目录(此处为 Nginx 默认安装目录,请根据实际情况操作)下。

6、编辑 Nginx 根目录下的 conf/nginx.conf文件。添加内容如下:

# https 服务配置
server {
	# 侦听80端口
	listen 443 default ssl;
	ssl on;
	#证书文件名称
	ssl_certificate 1_aidex.vip_bundle.crt; 
	#私钥文件名称
	ssl_certificate_key 2_aidex.vip.key; 
	# 定义访问域名
	server_name aidex.vip;
	location / {
		# 存放了静态页面的根目录
		root   /home/aidex/projects/static-web;
		# 默认主页
		index index.html;
	}
}

7、重启Nginx通过https访问 https://aidex.vip

8、如需把http的域名请求转成https,添加rewrite

rewrite ^(.*) https://$server_name$1 permanent;

9、解决重定向后https变成了http 的问题

proxy_redirect http:// https://; 

# 特殊字符串被过滤的解决办法

默认所有的都会过滤脚本,可以在application.yml配置xss.excludes属性排除URL

# 防止XSS攻击
xss: 
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes: /system/notice/*
  # 匹配链接
  urlPatterns: /system/*,/monitor/*,/tool/*

# 进入首页如何自动展开某菜单

例如,进入自动打开用户管理,调用applyPath,填入你请求菜单对应的url地址。 applyPath("/system/role")

# 进入首页如何默认记忆控制台

例如用户退出后,下次登陆系统,能默认打开之前工作路径。

可以在index.htmlindex-topnav.html,去掉window.performance.navigation.type == 1

if($.common.equals("history", mode) && window.performance.navigation.type == 1)

换成

if($.common.equals("history", mode))

# 打包如何分离jar包和资源文件

特殊情况需要分离lib和resouce可以修改aidex-admin 参考如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>aidex</artifactId>
        <groupId>com.aidex</groupId>
        <version>4.4.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
	<packaging>jar</packaging>
    <artifactId>aidex-admin</artifactId>
	
	<description>
	    web服务入口
	</description>

    <dependencies>
    
        <!-- SpringBoot集成thymeleaf模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- spring-boot-devtools -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional> <!-- 表示依赖不会传递 -->
		</dependency>

		<!-- swagger2-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
		</dependency>
		
		<!--防止进入swagger页面报类型转换错误,排除2.9.2中的引用,手动增加1.5.21版本-->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>
        
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>
		
		<!-- swagger2-UI-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
		</dependency>
		 
    	 <!-- Mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

		<!-- 核心模块-->
        <dependency>
            <groupId>com.aidex</groupId>
            <artifactId>aidex-framework</artifactId>
        </dependency>
        
        <!-- 定时任务-->
        <dependency>
            <groupId>com.aidex</groupId>
            <artifactId>aidex-quartz</artifactId>
        </dependency>
        
        <!-- 代码生成-->
        <dependency>
            <groupId>com.aidex</groupId>
            <artifactId>aidex-generator</artifactId>
        </dependency>
        
    </dependencies>

     <build>
        <!-- jar包名 -->
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- 分离lib -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!-- 依赖包输出目录,将来不打进jar包里 -->
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <excludeTransitive>false</excludeTransitive>
                            <stripVersion>false</stripVersion>
                            <includeScope>runtime</includeScope>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- copy资源文件 -->
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <resources>
                                <resource>
                                    <directory>src/main/resources</directory>
                                </resource>
                            </resources>
                            <outputDirectory>${project.build.directory}/resources</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- 打jar包时忽略配置文件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>**/*.yml</exclude>
                        <exclude>**/*.xml</exclude>
                    </excludes>
                </configuration>
            </plugin>
            <!-- spring boot repackage -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>ZIP</layout>
                    <includes>
                        <include>
                            <groupId>non-exists</groupId>
                            <artifactId>non-exists</artifactId>
                        </include>
                    </includes>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
	
</project>

TIP

提示

启动命令java -jar -Dloader.path=resources,lib aidex-admin.jar

# Linux系统验证码乱码解决方法

在云服务器(少许),或者干净的服务器上,服务器没有安装字体。

1、上传本地的 Arial.ttf字体

2、此时执行以下三个命令:(建立字体索引信息,更新字体缓存) mkfontscale mkfontdir fc-cache -fv

3、重新刷新你的页面

# 公共数据库定时任务没有被执行

经常会有小伙伴遇到定时任务没有被执行,或者执行了但是报错找不到对应的方法。

定时任务是分布式的,如果多个机器链接同一个数据库,定时任务会随机在某个机器上跑,所以有时候不是没有被执行,而是被其他机器上执行了。如果你的方法只在本机有,所以会提示找不到对应定时任务的方法。

这种情况或只有一台机器的话可以注释掉ScheduleConfig.java,这样的话就只会走本机(quartz相关定时任务的表也不需要),它也不会在去读quartz表走集群操作。

# 如何设置用户登录会话超时时间

找到aidex-admin\src\main\resources下面的application.yml配置文件

# Shiro
shiro:
  session:
    # Session超时时间,-1代表永不过期(默认30分钟)
    expireTime: 30

# 如何实现用户免密登录配置方法

免密使用的场景,例如短信验证码,第三方应用登录等。下面列出一个简单的实现方法,当然还有更多实现方式可以自己尝试。

1、新增一个登录类型枚举类LoginType

package com.aidex.framework.shiro.token;

/**
 * 登录类型枚举类
 * 
 * @author aidex
 */
public enum LoginType
{
    /**
     * 密码登录
     */
    PASSWORD("password"),
    /**
     * 免密码登录
     */
    NOPASSWD("nopasswd");

    private String desc;

    LoginType(String desc)
    {
        this.desc = desc;
    }

    public String getDesc()
    {
        return desc;
    }
}

2、自定义登录Token

package com.aidex.framework.shiro.token;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * 自定义登录Token
 * 
 * @author aidex
 */
public class UserToken extends UsernamePasswordToken
{
    private static final long serialVersionUID = 1L;

    private LoginType type;

    public UserToken()
    {
    }

    public UserToken(String username, String password, LoginType type, boolean rememberMe)
    {
        super(username, password, rememberMe);
        this.type = type;
    }

    public UserToken(String username, LoginType type)
    {
        super(username, "", false, null);
        this.type = type;
    }

    public UserToken(String username, String password, LoginType type)
    {
        super(username, password, false, null);
        this.type = type;
    }

    public LoginType getType()
    {
        return type;
    }

    public void setType(LoginType type)
    {
        this.type = type;
    }
}

3、对应Realm中添加登录类型判断,例如UserRealm(这里演示公用一个realm,如单独有免密realm不需要)

/**
 * 登录认证
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
{
	UserToken upToken = (UserToken) token;
	LoginType type = upToken.getType();
	String username = upToken.getUsername();
	String password = "";
	if (upToken.getPassword() != null)
	{
		password = new String(upToken.getPassword());
	}

	User user = null;
	try
	{
		if (LoginType.PASSWORD.equals(type))
		{
			user = loginService.login(username, password);
		}
		else if (LoginType.NOPASSWD.equals(type))
		{
			user = loginService.login(username);
		}
	}
	catch (CaptchaException e)
	{
		throw new AuthenticationException(e.getMessage(), e);
	}
	catch (UserNotExistsException e)
	{
		throw new UnknownAccountException(e.getMessage(), e);
	}
	catch (UserPasswordNotMatchException e)
	{
		throw new IncorrectCredentialsException(e.getMessage(), e);
	}
	catch (UserPasswordRetryLimitExceedException e)
	{
		throw new ExcessiveAttemptsException(e.getMessage(), e);
	}
	catch (UserBlockedException e)
	{
		throw new LockedAccountException(e.getMessage(), e);
	}
	catch (RoleBlockedException e)
	{
		throw new LockedAccountException(e.getMessage(), e);
	}
	catch (Exception e)
	{
		log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
		throw new AuthenticationException(e.getMessage(), e);
	}
	SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
	return info;
}

4、LoginService添加login方法,去掉密码验证。

/**
 * 登录
 */
public User login(String username)
{
	// 验证码校验
	if (!StringUtils.isEmpty(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA)))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
		throw new CaptchaException();
	}
	// 用户名或密码为空 错误
	if (StringUtils.isEmpty(username))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
		throw new UserNotExistsException();
	}

	// 用户名不在指定范围内 错误
	if (username.length() < UserConstants.USERNAME_MIN_LENGTH
			|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
		throw new UserPasswordNotMatchException();
	}

	// 查询用户信息
	User user = userService.selectUserByLoginName(username);

	if (user == null)
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
		throw new UserNotExistsException();
	}
	
	if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.delete")));
		throw new UserDeleteException();
	}
	
	if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark())));
		throw new UserBlockedException();
	}

	AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
	recordLoginInfo(user);
	return user;
}

5、在对应的登录方法中传入LoginType.NOPASSWD调用

UserToken token = new UserToken(username, LoginType.NOPASSWD);
Subject subject = SecurityUtils.getSubject();
subject.login(token);

# 如何处理Long类型精度丢失问题

当字段实体类为Long类型且值超过前端js显示的长度范围时会导致前端回显错误,解决方案如下

1、使用JsonSerialize注解序列化的时候把Long自动转为String(针对单个属性)

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

@JsonSerialize(using = ToStringSerializer.class)
private Long xxx;

2、添加JacksonConfig配置全局序列化(针对所有属性)

package com.aidex.framework.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

/**
 * Jackson配置
 * 
 * @author aidex
 *
 */
@Configuration
public class JacksonConfig
{
    @Bean
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter()
    {
        final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        final ObjectMapper objectMapper = builder.build();
        SimpleModule simpleModule = new SimpleModule();
        // Long 转为 String 防止 js 丢失精度
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        // 忽略 transient 关键词属性
        objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
        return new MappingJackson2HttpMessageConverter(objectMapper);
    }
}

# 如何修改超级管理员登录密码

1、如果是自己知道超级管理员的密码且需要修改的情况。 默认口令 admin/admin123,可以登录后在首页个人中心修改密码。

2、如果自己忘记了超级管理员的密码可以重新生成秘钥替换数据库密码。

public static void main(String[] args)
{
    // 第一个参数为账户名 第二个参数为密码 第三个参数为盐对应用户表salt(如果没有可以不用填)
	System.out.println(new PasswordService().encryptPassword("admin", "admin123", "111111"));
}

# 如何修改数据监控登录账户密码

控制台管理用户名和密码默认为aidex/123456

找到aidex-admin\src\main\resources下面的application-druid.yml配置文件

找到如下节点配置,设置控制台账号密码

# 控制台管理用户名和密码
login-username: 你的监控台账号
login-password: 你的监控台密码

# 前端静态资源如何整合到后端访问

分离版本都是前端和后端单独部署的,但是有些特殊情况想把前端静态资源整合到后端。提供如下方案:

1、修改aidex-ui中的.env.production(二选一)

// 本机地址访问
VUE_APP_BASE_API = '/'
// 任意地址访问
VUE_APP_BASE_API = '//localhost:8080'

2、修改aidex-ui中的router/index.js,设置mode属性为hash

export default new Router({
  mode: 'hash',
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

3、执行bin\build.bat打包前端静态资源文件。

4、修改后端resources中的application.yml,添加thymeleaf模板引擎配置

spring:
  # 模板引擎
  thymeleaf:
    mode: HTML
    encoding: utf-8
    cache: false

5、修改后端pom.xml,增加thymeleaf模板引擎依赖

<!-- spring-boot-thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

6、修改后端ResourcesConfig.java中的addResourceHandlers,添加静态资源映射地址

/** 前端静态资源配置 */
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");

7、修改后端SecurityConfig.java中的configure,添加允许访问的地址。

.antMatchers(
		HttpMethod.GET,
		"/*.html",
		"/**/*.html",
		"/**/*.css",
		"/**/*.js",
		"/static/**",
		"/",
		"/index"
).permitAll()

8、后端新建访问控制处理IndexController.java设置对应访问页面。

package com.aidex.project.system.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController
{
    // 系统首页
    @GetMapping(value = { "/", "/index", "/login" })
    public String index()
    {
        return "index";
    }
}

9、整合前端dist静态资源文件到后端

后端resources下新建templates目录,复制静态页面index.html过来。

复制静态文件staticresources目录下。

10、启动测试访问地址

打开浏览器,输入:http://localhost:8080 能正常访问和登录表示成功。

# 登录出现DatatypeConverter异常

错误提示:Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

由于>= jdk9中不再包含这个jar包,所以需要在aidex-common\pom.xml手动添加依赖。

# 如何解决导出使用下载插件出现异常

导出文件的逻辑是先创建一个临时文件,等待前端请求下载结束后马上删除这个临时文件。但是有些下载插件,例如迅雷(他们是二次下载),这个时候文件已经删除,会导致异常,找不到文件。

解决方案:如果有硬性要求话,可以把所有的导出都改成流的形式返回给前端,不采用临时文件的方法。

// 默认的post请求 生成临时文件
@PostMapping("/export")
@ResponseBody
public AjaxResult export(Xxxx xxxx)
{
	List<Xxxx> list = xxxxService.selectXxxxList(xxxx);
	ExcelUtil<Xxxx> util = new ExcelUtil<Xxxx>(Xxxx.class);
	return util.exportExcel(list, "xxxx");
}

修改相关导出文件java代码aidex-ui.js通用导出方法

// 通过流的形式返回给前端
@GetMapping("/export")
@ResponseBody
public void export(HttpServletResponse response, Xxxx xxxx) throws IOException
{
	List<Xxxx> list = xxxxService.selectXxxxList(xxxx);
	ExcelUtil<Xxxx> util = new ExcelUtil<Xxxx>(Xxxx.class);
	util.exportExcel(response, list, "xxxx");
}
// ry-ui.js导出数据修改成get请求方式
exportExcel: function(formId) {
	table.set();
	$.modal.confirm("确定导出所有" + table.options.modalName + "吗?", function() {
		var currentId = $.common.isEmpty(formId) ? $('form').attr('id') : formId;
		var params = $("#" + table.options.id).bootstrapTable('getOptions');
		var dataParam = $("#" + currentId).serializeArray();
		dataParam.push({ "name": "orderByColumn", "value": params.sortName });
		dataParam.push({ "name": "isAsc", "value": params.sortOrder });
		window.location.href = table.options.exportUrl + "?" + $.param(dataParam);
	});
},