Ruoyi Eleadmin 在线文档Ruoyi Eleadmin 在线文档
首页
文档
Mybatis版本
SpringData JPA版本
  • Mybatis版本
  • SpringData JPA版本
  • github
  • gitee
首页
文档
Mybatis版本
SpringData JPA版本
  • Mybatis版本
  • SpringData JPA版本
  • github
  • gitee
  • RuoYi EleAdmin

介绍


Eleadmin 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、SpringData Jpa、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置,支持集群,支持多数据源,支持分布式事务等。

在线体验

  • eleadmin官网:https://www.eleadmin.cn(opens new window)

SpringData JPA 版本

  • eleadmin SpringData JPA 版本:https://cms.eleadmin.cn(opens new window)

Mybatis 版本

  • eleadmin Mybatis 版本:https://demo.eleadmin.cn(opens new window)

快速了解

项目简介

Eleadmin是一款基于SpringBoot+Vue的前后端分离极速后台开发框架。

  • eleadmin 官网地址:https://www.eleadmin.cn(opens new window)
  • eleadmin 在线文档:https://doc.eleadmin.cn(opens new window)

Eleadmin 是一个 Java EE 企业级快速开发平台,基于经典技术组合(Spring Boot、Spring Security、SpringData Jpa、Jwt、Vue),内置模块如:部门管理、角色用户、菜单及按钮授权、数据权限、系统参数、日志管理、通知公告、代码生成等。在线定时任务配置;支持集群,支持多数据源,支持分布式事务。

主要特性

  • 完全响应式布局(支持电脑、平板、手机等所有主流设备)
  • 强大的一键生成功能(包括控制器、模型、视图、菜单等)
  • 支持多数据源,简单配置即可实现切换。
  • 支持按钮及数据权限,可自定义部门数据权限。
  • 对常用js插件进行二次封装,使js代码变得简洁,更加易维护
  • 完善的XSS防范及脚本过滤,彻底杜绝XSS攻击
  • Maven多项目依赖,模块及插件分项目,尽量松耦合,方便模块升级、增减模块。
  • 国际化支持,服务端及客户端支持
  • 完善的日志记录体系简单注解即可实现
  • 支持服务监控,数据监控,缓存监控功能。

技术选型 - Mytatis 版本

1、系统环境

  • Java EE 8
  • Servlet 3.0
  • Apache Maven 3

2、主框架

  • Spring Boot 2.2.x
  • Spring Framework 5.2.x
  • Spring Security 5.2.x

3、持久层

  • Apache MyBatis 3.5.x
  • Hibernate Validation 6.0.x
  • Alibaba Druid 1.2.x

4、视图层

  • Vue 2.6.x
  • Axios 0.21.x
  • Element 2.15.x

技术选型 - SpringData JPA 版本

1、系统环境

  • Java EE 21
  • Servlet 3.0
  • Apache Maven 3

2、主框架

  • Spring Boot 3.5.10
  • Spring Framework 5.2.x
  • Spring Security 5.2.x

3、持久层

  • Apache MyBatis 3.5.x
  • Hibernate Validation 6.0.x
  • Alibaba Druid 1.2.x

4、视图层

  • Vue 2.6.x
  • Axios 0.21.x
  • Element 2.15.x

内置功能

  • 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
  • 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
  • 岗位管理:配置系统用户所属担任职务。
  • 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  • 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  • 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
  • 参数管理:对系统动态配置常用参数。
  • 通知公告:系统通知公告信息发布维护。
  • 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  • 登录日志:系统登录日志记录查询包含登录异常。
  • 在线用户:当前系统中活跃用户状态监控。
  • 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
  • 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
  • 系统接口:根据业务代码自动生成相关的api接口文档。
  • 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
  • 缓存监控:对系统的缓存信息查询,命令统计等。
  • 在线构建器:拖动表单元素生成相应的Vue代码。
  • 连接池监视:监视当期系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。

环境部署

准备工作

JDK >= 1.8 (推荐1.8版本)
Mysql >= 5.7.0 (推荐5.7版本)
Redis >= 3.0
Maven >= 3.0
Node >= 12

提示

前端安装完node后,最好设置下淘宝的镜像源,不建议使用cnpm(可能会出现奇怪的问题)

运行系统

后端运行

1、导入到Idea,菜单 File -> Import,然后选择 Maven -> Existing Maven Projects,点击 Next> 按钮,选择工作目录,然后点击 Finish 按钮,即可成功导入。 Idea会自动加载Maven依赖包,初次加载会比较慢(根据自身网络情况而定) 2、创建数据库ry-vue并导入数据脚本ry_2021xxxx.sql,quartz.sql 3、打开项目运行com.ruoyi.RuoYiApplication.java,出现如下图表示启动成功。

(♥◠‿◠)ノ゙  若依启动成功   ლ(´ڡ`ლ)゙
 .-------.       ____     __
 |  _ _   \      \   \   /  /
 | ( ' )  |       \  _. /  '
 |(_ o _) /        _( )_ .'
 | (_,_).' __  ___(_ o _)'
 |  |\ \  |  ||   |(_,_)'
 |  | \ `'   /|   `-'  /
 |  |  \    /  \      /
 ''-'   `'-'    `-..-'

提示

后端运行成功可以通过(http://localhost:8080 (opens new window))访问,但是不会出现静态页面,可以继续参考下面步骤部署ruoyi-ui前端,然后通过前端地址来访问。

前端运行

# 进入项目目录
cd ruoyi-ui

# 安装依赖
npm install

# 强烈建议不要用直接使用 cnpm 安装,会有各种诡异的 bug,可以通过重新指定 registry 来解决 npm 安装速度慢的问题。
npm install --registry=https://registry.npmmirror.com

# 本地开发 启动项目
npm run dev

4、打开浏览器,输入:(http://localhost:80 (opens new window)) 默认账户/密码 admin/admin123) 若能正确展示登录页面,并能成功登录,菜单及页面展示正常,则表明环境搭建成功

建议使用Git克隆,因为克隆的方式可以和RuoYi-Vue随时保持更新同步。使用Git命令克隆

git clone https://gitee.com/y_project/RuoYi-Vue.git

如需要使用SpringBoot3,JDK17+版本,使用Git命令切换,代码和RuoYi-Vue保持更新同步。

git checkout springboot3

提示

因为本项目是前后端完全分离的,所以需要前后端都单独启动好,才能进行访问。 前端安装完node后,最好设置下淘宝的镜像源,不建议使用cnpm(可能会出现奇怪的问题)

必要配置

  • 修改数据库连接,编辑resources目录下的application-druid.yml
# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      master:
        url: 数据库地址
        username: 数据库账号
        password: 数据库密码
  • 修改服务器配置,编辑resources目录下的application.yml
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为80
  port: 端口
  servlet:
    # 应用的访问路径
    context-path: /应用路径

部署系统

后端部署

  • 打包工程文件

在ruoyi项目的bin目录下执行package.bat打包Web工程,生成war/jar包文件。 然后会在项目下生成target文件夹包含war或jar

提示

多模块版本会生成在ruoyi/ruoyi-admin模块下target文件夹

  • 部署工程文件

1、jar部署方式 使用命令行执行:java –jar ruoyi.jar 或者执行脚本:ruoyi/bin/run.bat

2、war部署方式 ruoyi/pom.xml中的packaging修改为war,放入tomcat服务器webapps

   <packaging>war</packaging>

提示

多模块版本在ruoyi/ruoyi-admin模块下修改pom.xml

  • SpringBoot去除内嵌Tomcat(PS:此步骤不重要,因为不排除也能在容器中部署war)
<!-- 多模块排除内置tomcat -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<!-- 单应用排除内置tomcat -->
<exclusions>
	<exclusion>
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<groupId>org.springframework.boot</groupId>
	</exclusion>
</exclusions>

前端部署

当项目开发完毕,只需要运行一行命令就可以打包你的应用

# 打包正式环境
npm run build:prod

# 打包预发布环境
npm run build:stage

构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件,通常是 ***.js 、***.css、index.html 等静态文件。

通常情况下 dist 文件夹的静态文件发布到你的 nginx 或者静态服务器即可,其中的 index.html 是后台服务的入口页面。

outputDir 提示

如果需要自定义构建,比如指定 dist 目录等,则需要通过 config (opens new window)的 outputDir 进行配置。

publicPath 提示

部署时改变页面js 和 css 静态引入路径 ,只需修改 vue.config.js 文件资源路径即可。

publicPath: "./"; //请根据自己路径来配置更改
export default new Router({
  mode: "hash", // hash模式
});

环境变量

所有测试环境或者正式环境变量的配置都在 .env.development (opens new window)等 .env.xxxx文件中。

它们都会通过 webpack.DefinePlugin 插件注入到全局。

环境变量必须以VUE_APP_为开头。如:VUE_APP_API、VUE_APP_TITLE

你在代码中可以通过如下方式获取:

console.log(process.env.VUE_APP_xxxx);

扩展阅读:《Vue CLI - 环境变量和模式》(opens new window)

注意

环境配置修改后,需要重新运行才会生效

Tomcat配置

修改server.xml,Host节点下添加

<Context docBase="" path="/" reloadable="true" source=""/>

dist目录的文件夹下新建WEB-INF文件夹,并在里面添加web.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
        version="3.1" metadata-complete="true">
     <display-name>Router for Tomcat</display-name>
     <error-page>
        <error-code>404</error-code>
        <location>/index.html</location>
    </error-page>
</web-app>

Nginx配置

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;
		charset utf-8;

		location / {
            root   /home/ruoyi/projects/ruoyi-ui;
			try_files $uri $uri/ /index.html;
            index  index.html index.htm;
        }

		location /prod-api/ {
			proxy_set_header Host $http_host;
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header REMOTE-HOST $remote_addr;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_pass http://localhost:8080/;
		}

		# springdoc proxy
		location ~ ^/v3/api-docs/(.*) {
			proxy_pass http://localhost:8080/v3/api-docs/$1;
		}

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
建议开启Gzip压缩

在http配置中加入如下代码对全局的资源进行压缩,可以减少文件体积和加快网页访问速度。

# 开启gzip压缩
gzip on;
# 不压缩临界值,大于1K的才压缩,一般不用改
gzip_min_length 1k;
# 压缩缓冲区
gzip_buffers 16 64K;
# 压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
gzip_http_version 1.1;
# 压缩级别,1-10,数字越大压缩的越好,时间也越长
gzip_comp_level 5;
# 进行压缩的文件类型
gzip_types text/plain application/x-javascript text/css application/xml application/javascript;
# 跟Squid等缓存服务有关,on的话会在Header里增加"Vary: Accept-Encoding"
gzip_vary on;
# IE6对Gzip不怎么友好,不给它Gzip了
gzip_disable "MSIE [1-6]\.";

同时建议开启解压缩静态文件 如何使用Gzip解压缩静态文件

常见问题

  1. 如果使用Mac需要修改application.yml文件路径profile
  2. 如果使用Linux 提示表不存在,设置大小写敏感配置在/etc/my.cnf添加lower_case_table_names=1,重启MYSQL服务
  3. 如果提示当前权限不足,无法写入文件请检查application.yml中的profile路径或logback.xml中的log.path路径是否有可读可写操作权限

如遇到无法解决的问题请到Issues (opens new window)反馈,会不定时进行解答

项目介绍

文件结构

后端结构

com.ruoyi
├── common            // 工具类
│       └── annotation                    // 自定义注解
│       └── config                        // 全局配置
│       └── constant                      // 通用常量
│       └── core                          // 核心控制
│       └── enums                         // 通用枚举
│       └── exception                     // 通用异常
│       └── filter                        // 过滤器处理
│       └── utils                         // 通用类处理
├── framework         // 框架核心
│       └── aspectj                       // 注解实现
│       └── config                        // 系统配置
│       └── datasource                    // 数据权限
│       └── interceptor                   // 拦截器
│       └── manager                       // 异步处理
│       └── security                      // 权限控制
│       └── web                           // 前端控制
├── ruoyi-generator   // 代码生成(可移除)
├── ruoyi-quartz      // 定时任务(可移除)
├── ruoyi-system      // 系统代码
├── ruoyi-admin       // 后台服务
├── ruoyi-xxxxxx      // 其他模块

前端结构

├── build                      // 构建相关
├── bin                        // 执行脚本
├── public                     // 公共文件
│   ├── favicon.ico            // favicon图标
│   └── index.html             // html模板
│   └── robots.txt             // 反爬虫
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
│   ├── components             // 全局公用组件
│   ├── directive              // 全局指令
│   ├── layout                 // 布局
│   ├── plugins                // 通用方法
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── utils                  // 全局公用方法
│   ├── views                  // view
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   ├── permission.js          // 权限管理
│   └── settings.js            // 系统配置
├── .editorconfig              // 编码格式
├── .env.development           // 开发环境配置
├── .env.production            // 生产环境配置
├── .env.staging               // 测试环境配置
├── .eslintignore              // 忽略语法检查
├── .eslintrc.js               // eslint 配置项
├── .gitignore                 // git 忽略项
├── babel.config.js            // babel.config.js
├── package.json               // package.json
└── vue.config.js              // vue.config.js

配置文件

通用配置 application.yml

# 项目相关配置
ruoyi:
  # 名称
  name: RuoYi
  # 版本
  version: 3.3.0
  # 版权年份
  copyrightYear: 2021
  # 实例演示开关
  demoEnabled: true
  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: D:/ruoyi/uploadPath
  # 获取ip地址开关
  addressEnabled: false
  # 验证码类型 math 数组计算 char 字符验证
  captchaType: math

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8080
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30

# 日志配置
logging:
  level:
    com.ruoyi: debug
    org.springframework: warn

# Spring配置
spring:
  # 资源信息
  messages:
    # 国际化资源文件路径
    basename: i18n/messages
  profiles:
    active: druid
  # 文件上传
  servlet:
    multipart:
      # 单个文件大小
      max-file-size: 10MB
      # 设置总上传的文件大小
      max-request-size: 20MB
  # 服务模块
  devtools:
    restart:
      # 热部署开关
      enabled: true
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

# token配置
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: abcdefghijklmnopqrstuvwxyz
  # 令牌有效期(默认30分钟)
  expireTime: 30

# MyBatis配置
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.ruoyi.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml

# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

# Swagger配置
swagger:
  # 是否开启swagger
  enabled: true
  # 请求前缀
  pathMapping: /dev-api

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

数据源配置 application-druid.yml

# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: password
      # 从库数据源
      slave:
        # 从数据源开关/默认关闭
        enabled: false
        url:
        username:
        password:
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username:
        login-password:
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true

核心技术

TIP

  • 前端技术栈 ES6、vue、vuex、vue-router、vue-cli、axios、element-ui
  • 后端技术栈 SpringBoot、MyBatis、Spring Security、Jwt

后端技术

SpringBoot框架

1、介绍 Spring Boot是一款开箱即用框架,提供各种默认配置来简化项目配置。让我们的Spring应用变的更轻量化、更快的入门。 在主程序执行main函数就可以运行。你也可以打包你的应用为jar并通过使用java -jar来运行你的Web应用。它遵循"约定优先于配置"的原则, 使用SpringBoot只需很少的配置,大部分的时候直接使用默认的配置即可。同时可以与Spring Cloud的微服务无缝结合。

提示

Spring Boot2.x版本环境要求必须是jdk8或以上版本,服务器Tomcat8或以上版本

2、优点

  • 使编码变得简单: 推荐使用注解。
  • 使配置变得简单: 自动配置、快速集成新技术能力 没有冗余代码生成和XML配置的要求
  • 使部署变得简单: 内嵌Tomcat、Jetty、Undertow等web容器,无需以war包形式部署
  • 使监控变得简单: 提供运行时的应用监控
  • 使集成变得简单: 对主流开发框架的无配置集成。
  • 使开发变得简单: 极大地提高了开发快速构建项目、部署效率。

Spring Security安全控制

1、介绍 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

2、功能 Authentication 认证,就是用户登录 Authorization 授权,判断用户拥有什么权限,可以访问什么资源 安全防护,跨站脚本攻击,session攻击等 非常容易结合Spring进行使用

3、Spring Security与Shiro的区别

相同点

1、认证功能 2、授权功能 3、加密功能 4、会话管理 5、缓存支持 6、rememberMe功能 ....

不同点

优点:

1、Spring Security基于Spring开发,项目如果使用Spring作为基础,配合Spring Security做权限更加方便。而Shiro需要和Spring进行整合开发 2、Spring Security功能比Shiro更加丰富,例如安全防护方面 3、Spring Security社区资源相对比Shiro更加丰富

缺点:

1)Shiro的配置和使用比较简单,Spring Security上手复杂些 2)Shiro依赖性低,不需要依赖任何框架和容器,可以独立运行。Spring Security依赖Spring容器

前端技术

  • npm:node.js的包管理工具,用于统一管理我们前端项目中需要用到的包、插件、工具、命令等,便于开发和维护。
  • ES6:Javascript的新版本,ECMAScript6的简称。利用ES6我们可以简化我们的JS代码,同时利用其提供的强大功能来快速实现JS逻辑。
  • vue-cli:Vue的脚手架工具,用于自动生成Vue项目的目录及文件。
  • vue-router: Vue提供的前端路由工具,利用其我们实现页面的路由控制,局部刷新及按需加载,构建单页应用,实现前后端分离。
  • vuex:Vue提供的状态管理工具,用于统一管理我们项目中各种数据的交互和重用,存储我们需要用到数据对象。
  • element-ui:基于MVVM框架Vue开源出来的一套前端ui组件。

后台手册

分页实现

前端调用实现

1、前端定义分页流程

// 一般在查询参数中定义分页变量
queryParams: {
  pageNum: 1,
  pageSize: 10
},

// 页面添加分页组件,传入分页变量
<pagination
  v-show="total>0"
  :total="total"
  :page.sync="queryParams.pageNum"
  :limit.sync="queryParams.pageSize"
  @pagination="getList"
/>

// 调用后台方法,传入参数 获取结果
listUser(this.queryParams).then(response => {
    this.userList = response.rows;
    this.total = response.total;
  }
);

后台逻辑实现

@PostMapping("/list")
@ResponseBody
public TableDataInfo list(User user)
{
    startPage();  // 此方法配合前端完成自动分页
    List<User> list = userService.selectUserList(user);
    return getDataTable(list);
}
  • 常见坑点1:selectPostById莫名其妙的分页。例如下面这段代码
startPage();
List<User> list;
if(user != null){
    list = userService.selectUserList(user);
} else {
    list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);

原因分析:这种情况下由于user存在null的情况,就会导致pageHelper生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。 当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。 上面这个代码,应该写成下面这个样子才能保证安全。

List<User> list;
if(user != null){
	startPage();
	list = userService.selectUserList(user);
} else {
	list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
  • 常见坑点2:添加了startPage方法。也没有正常分页。例如下面这段代码
startPage();
Post post = postService.selectPostById(1L);
List<User> list = userService.selectUserList(user);
return getDataTable(list);

原因分析:只对该语句以后的第一个查询(Select)语句得到的数据进行分页。 上面这个代码,应该写成下面这个样子才能正常分页。

Post post = postService.selectPostById(1L);
startPage();
List<User> list = userService.selectUserList(user);
return getDataTable(list);

提示

项目分页插件默认是Mysql语法,如果项目改为其他数据库需修改配置application.yml文件中的属性helperDialect: 你的数据库

注意

只要你可以保证在PageHelper方法调用后紧跟MyBatis查询方法,这就是安全的。因为PageHelper在finally代码段中自动清除了ThreadLocal存储的对象。 如果代码在进入Executor前发生异常,就会导致线程不可用,这属于人为的Bug(例如接口方法和XML中的不匹配,导致找不到MappedStatement时),这种情况由于线程不可用,也不会导致ThreadLocal参数被错误的使用。

导入导出

注解参数说明

参数类型默认值描述
sortintInteger.MAX_VALUE导出时在excel中排序,值越小越靠前
nameString空导出到Excel中的名字
dateFormatString空日期格式, 如: yyyy-MM-dd
dictTypeString空如果是字典类型,请设置字典的type值 (如: sys_user_sex)
readConverterExpString空读取内容转表达式 (如: 0=男,1=女,2=未知)
separatorString,分隔符,读取字符串组内容
scaleint-1BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
roundingModeintBigDecimal.ROUND_HALF_EVENBigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
celltypeEnumType.STRING导出类型(0数字 1字符串 2图片)
heightString14导出时在excel中每个列的高度 单位为字符
widthString16导出时在excel中每个列的宽 单位为字符
suffixString空文字后缀,如% 90 变成90%
defaultValueString空当值为空时,字段的默认值
promptString空提示信息
wrapTextbooleanfalse是否允许内容换行
comboStringNull设置只能选择不能输入的列内容
comboReadDictbooleanfalse是否从字典读数据到combo,默认不读取,如读取需要设置dictType注解.
headerBackgroundColorEnumIndexedColors.GREY_50_PERCENT导出列头背景色IndexedColors.XXXX
headerColorEnumIndexedColors.WHITE导出列头字体颜色IndexedColors.XXXX
backgroundColorEnumIndexedColors.WHITE导出单元格背景色IndexedColors.XXXX
colorEnumIndexedColors.BLACK导出单元格字体颜色IndexedColors.XXXX
targetAttrString空另一个类中的属性名称,支持多级获取,以小数点隔开
isStatisticsbooleanfalse是否自动统计数据,在最后追加一行统计数据总和
typeEnumType.ALL字段类型(0:导出导入;1:仅导出;2:仅导入)
alignEnumHorizontalAlignment.CENTER导出对齐方式HorizontalAlignment.XXXX
handlerClassExcelHandlerAdapter.class自定义数据处理器
argsString[]{}自定义数据处理器参数

导出实现流程

1、前端调用封装好的方法$.table.init,传入后台exportUrl

var options = {
  exportUrl: prefix + "/export",
  columns: [
    {
      field: "id",
      title: "主键",
    },
    {
      field: "name",
      title: "名称",
    },
  ],
};
$.table.init(options);

2、添加导出按钮事件

<a class="btn btn-warning" onclick="$.table.exportExcel()">
  <i class="fa fa-download"></i> 导出
</a>

3、在实体变量上添加@Excel注解

@Excel(name = "用户序号", prompt = "用户编号")
private Long userId;

@Excel(name = "用户名称")
private String userName;

@Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
private String sex;

@Excel(name = "用户头像", cellType = ColumnType.IMAGE)
private String avatar;

@Excel(name = "帐号状态", dictType = "sys_normal_disable")
private String status;

@Excel(name = "最后登陆时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date loginDate;

4、在Controller添加导出方法

@PostMapping("/export")
@ResponseBody
public AjaxResult export(User user)
{
	List<User> list = userService.selectUserList(user);
	ExcelUtil<User> util = new ExcelUtil<User>(User.class);
	return util.exportExcel(list, "用户数据");
}

提示

导出默认流程是先创建一个临时文件,等待前端请求下载结束后马上删除这个临时文件。如遇到迅雷这种二次请求下载应用可能会导致文件已经被删除,我们也可以改成流的形式返回给前端。 参考实现 - 如何解决导出使用下载插件出现异常

导入实现流程

1、前端调用封装好的方法$.table.init,传入后台importUrl。

var options = {
  importUrl: prefix + "/importData",
  columns: [
    {
      field: "id",
      title: "主键",
    },
    {
      field: "name",
      title: "名称",
    },
  ],
};
$.table.init(options);

2、添加导入按钮事件

<a class="btn btn-info" onclick="$.table.importExcel()">
  <i class="fa fa-upload"></i> 导入
</a>

3、添加导入前端代码,form默认id为importForm,也可指定importExcel(id)

<!-- 导入区域 -->
<script id="importTpl" type="text/template">
  <form enctype="multipart/form-data" class="mt20 mb10">
  	<div class="col-xs-offset-1">
  		<input type="file" id="file" name="file"/>
  		<div class="mt10 pt5">
  			<input type="checkbox" id="updateSupport" name="updateSupport" title="如果登录账户已经存在,更新这条数据。"> 是否更新已经存在的用户数据
  			 &nbsp;	<a onclick="$.table.importTemplate()" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> 下载模板</a>
  		</div>
  		<font color="red" class="pull-left mt10">
  			提示:仅允许导入“xls”或“xlsx”格式文件!
  		</font>
  	</div>
  </form>
</script>

4、在实体变量上添加@Excel注解,默认为导出导入,也可以单独设置仅导入Type.IMPORT

@Excel(name = "用户序号")
private Long id;

@Excel(name = "部门编号", type = Type.IMPORT)
private Long deptId;

@Excel(name = "用户名称")
private String userName;

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

/** 导出部门单个对象 */
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT)
private SysDept dept;

5、在Controller添加导入方法,updateSupport属性为是否存在则覆盖(可选)

@PostMapping("/importData")
@ResponseBody
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
	ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
	List<SysUser> userList = util.importExcel(file.getInputStream());
	String operName = ShiroUtils.getSysUser().getLoginName();
	String message = userService.importUser(userList, updateSupport, operName);
	return AjaxResult.success(message);
}

提示

也可以直接到main运行此方法测试。

InputStream is = new FileInputStream(new File("D:\\test.xlsx"));
ExcelUtil<Entity> util = new ExcelUtil<Entity>(Entity.class);
List<Entity> userList = util.importExcel(is);

自定义标题信息

有时候我们希望导出表格包含标题信息,我们可以这样做。

导出用户管理表格新增标题(用户列表)

public AjaxResult export(SysUser user)
{
	List<SysUser> list = userService.selectUserList(user);
	ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
	return util.exportExcel(list, "用户数据", "用户列表");
}

导入表格包含标题处理方式,其中1表示标题占用行数,根据实际情况填写。

public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
	ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
	List<SysUser> userList = util.importExcel(file.getInputStream(), 1);
	String operName = SecurityUtils.getUsername();
	String message = userService.importUser(userList, updateSupport, operName);
	return AjaxResult.success(message);
}

自定义数据处理器

有时候我们希望数据展现为一个特殊的格式,或者需要对数据进行其它处理。Excel注解提供了自定义数据处理器以满足各种业务场景。而实现一个数据处理器也是非常简单的。如下:

1、在实体类用Excel注解handler属性指定自定义的数据处理器

public class User extends BaseEntity
{
    @Excel(name = "用户名称", handler = MyDataHandler.class, args = { "aaa", "bbb" })
    private String userName;
}

2、编写数据处理器MyDataHandler继承ExcelHandlerAdapter,返回值为处理后的值。

public class MyDataHandler implements ExcelHandlerAdapter
{
    @Override
    public Object format(Object value, String[] args, Cell cell, Workbook wb)
    {
        // value 为返回单元格显示内容值
		// args 为excel注解传递的args数组值
		// cell 为单元格对象
		// wb 为工作簿对象
		return value;
    }
}

3、编写示例,用户名为若依则单元格文字设置为红色

public class MyDataHandler implements ExcelHandlerAdapter
{
    @Override
    public Object format(Object value, String[] args, Cell cell, Workbook wb)
    {
        if ("若依".equals(value))
        {
            // 自定义用户名为若依/单元格文字设置为红色
            CellStyle style = wb.createCellStyle();
            style.setAlignment(HorizontalAlignment.CENTER);
            style.setVerticalAlignment(VerticalAlignment.CENTER);
            style.setBorderRight(BorderStyle.THIN);
            style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderLeft(BorderStyle.THIN);
            style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderTop(BorderStyle.THIN);
            style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            style.setBorderBottom(BorderStyle.THIN);
            style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
            Font dataFont = wb.createFont();
            dataFont.setFontName("Arial");
            dataFont.setFontHeightInPoints((short) 10);
            dataFont.setColor(IndexedColors.RED.index);
            style.setFont(dataFont);
            cell.setCellStyle(style);
        }
        return value;
    }
}

导出文件结果 ruoyi-color-execl

自定义显示属性列

有时候我们在很多属性字段中都加了Excel注解,但仅希望在Excel文件中显示指定列属性,那么我们可以进行如下处理。

示例:导出仅显示列信息。

@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{
	List<SysUser> list = userService.selectUserList(user);
	ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
	util.showColumn("loginName", "email", "dept.deptName");
	util.exportExcel(response, list, "用户数据");
}

自定义隐藏属性列

有时候我们希望对列信息根据业务去动态显示,那么我们可以进行如下处理。

示例:对用户进行条件判断,符合条件则隐藏属性。导出的文件则不会显示此列信息。

@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{
	List<SysUser> list = userService.selectUserList(user);
	ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
	if (条件A) {
	  // 不显示用户ID(单个)
	  util.hideColumn("userId");
	} else if (条件B) {
	  // 不显示用户名称、用户手机(多个)
	  util.hideColumn("userId", "phonenumber");
	} } else if (条件C) {
	  // 不显示用户邮箱、部门名称(子对象)
	  util.hideColumn("email", "dept.deptName");
	}
	util.exportExcel(response, list, "用户数据");
}

导入对象的子对象

有时候我们导入对象里面还包含对象,例如用户管理包含部门需要导入,那么我们可以进行如下处理。

/**
 * 用户对象 sys_user
 *
 * @author ruoyi
 */
public class SysUser extends BaseEntity
{
    ...............

	/** 部门对象 */
	@Excels({
	  @Excel(name = "部门名称", targetAttr = "deptName"),
	  @Excel(name = "部门负责人", targetAttr = "leader"),
	  @Excel(name = "部门状态", targetAttr = "status", dictType = "sys_normal_disable")
	})
	private SysDept dept = new SysDept();
}

导出对象的子对象

有时候对象里面还包含集合列表,例如用户管理包含多个角色需要导出,那么我们可以进行如下处理。

SysUser.java

public class SysUser
{
    @Excel(name = "用户编号", cellType = ColumnType.NUMERIC, width = 20, needMerge = true)
    private String userId;

    @Excel(name = "用户名称", width = 20, needMerge = true)
    private String userName;

    @Excel(name = "邮箱", width = 20, needMerge = true)
    private String email;

    @Excel(name = "角色")
    private List<SysRole> roles;

    public String getUserId()
    {
        return userId;
    }

    public void setUserId(String userId)
    {
        this.userId = userId;
    }

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        this.userName = userName;
    }

    public String getEmail()
    {
        return email;
    }

    public void setEmail(String email)
    {
        this.email = email;
    }

    public List<SysRole> getRoles()
    {
        return roles;
    }

    public void setRoles(List<SysRole> roles)
    {
        this.roles = roles;
    }
}

SysRole.java

public class SysRole
{
    @Excel(name = "角色编号", cellType = ColumnType.NUMERIC)
    private String roleId;

    @Excel(name = "角色名称")
    private String roleName;

    @Excel(name = "角色字符")
    private String roleKey;

    public String getRoleId()
    {
        return roleId;
    }

    public void setRoleId(String roleId)
    {
        this.roleId = roleId;
    }

    public String getRoleName()
    {
        return roleName;
    }

    public void setRoleName(String roleName)
    {
        this.roleName = roleName;
    }

    public String getRoleKey()
    {
        return roleKey;
    }

    public void setRoleKey(String roleKey)
    {
        this.roleKey = roleKey;
    }

}

测试验证

public class Test
{
    public static void main(String[] args) throws IOException
    {
        List<SysUser> userList = new ArrayList<SysUser>();

        SysUser user1 = new SysUser();
        List<SysRole> roles1 = new ArrayList<SysRole>();

        SysRole role1 = new SysRole();
        role1.setRoleId("1");
        role1.setRoleName("超级管理员");
        role1.setRoleKey("admin_key");

        SysRole role2 = new SysRole();
        role2.setRoleId("2");
        role2.setRoleName("普通角色");
        role2.setRoleKey("common_key");

        SysRole role3 = new SysRole();
        role3.setRoleId("3");
        role3.setRoleName("测试角色");
        role3.setRoleKey("test_key");

        SysRole role4 = new SysRole();
        role4.setRoleId("4");
        role4.setRoleName("查询角色");
        role4.setRoleKey("query_key");

        roles1.add(role1);
        roles1.add(role2);
        roles1.add(role3);
        roles1.add(role4);

        user1.setUserId("1");
        user1.setUserName("admin");
        user1.setEmail("ry@qq.com");
        user1.setRoles(roles1);

        userList.add(user1);


        SysUser user2 = new SysUser();
        List<SysRole> roles2 = new ArrayList<SysRole>();

        SysRole role21 = new SysRole();
        role21.setRoleId("4");
        role21.setRoleName("研发角色");
        role21.setRoleKey("yanfa_key");

        SysRole role22 = new SysRole();
        role22.setRoleId("5");
        role22.setRoleName("销售角色");
        role22.setRoleKey("xiaoshou_key");

        roles2.add(role21);
        roles2.add(role22);

        user2.setUserId("2");
        user2.setUserName("ry");
        user2.setEmail("admin@qq.com");
        user2.setRoles(roles2);

        userList.add(user2);

        SysUser user3 = new SysUser();
        List<SysRole> roles3 = new ArrayList<SysRole>();

        SysRole role31 = new SysRole();
        role31.setRoleId("4");
        role31.setRoleName("张三角色");
        role31.setRoleKey("zs_key");

        SysRole role32 = new SysRole();
        role32.setRoleId("5");
        role32.setRoleName("李四角色");
        role32.setRoleKey("ls_key");

        roles3.add(role31);
        roles3.add(role32);

        user3.setUserId("3");
        user3.setUserName("test");
        user3.setEmail("test@qq.com");
        user3.setRoles(roles3);

        userList.add(user3);

        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        AjaxResult ajax = util.exportExcel(userList, "用户数据", "用户数据");
        System.out.println(ajax.toString());
    }
}

导出文件结果 ruoyi-sub-execl

上传下载

上传实现流程

1、代码生成sys_file_info表相关代码并复制到对应目录。

2、参考示例修改代码。

<input id="filePath" name="filePath" class="form-control" type="file" />
function submitHandler() { if ($.validate.form()) { uploadFile(); } } function
uploadFile() { var formData = new FormData(); if ($('#filePath')[0].files[0] ==
null) { $.modal.alertWarning("请先选择文件路径"); return false; }
formData.append('fileName', $("input[name='fileName']").val());
formData.append('file', $('#filePath')[0].files[0]); $.ajax({ url: prefix +
"/add", type: 'post', cache: false, data: formData, processData: false,
contentType: false, dataType: "json", success: function(result) {
$.operate.successCallback(result); } }); }

3、在SysFileInfoController添加对应上传方法

@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@RequestParam("file") MultipartFile file, SysFileInfo fileInfo) throws IOException
{
	// 上传文件路径
	String filePath = RuoYiConfig.getUploadPath();
	// 上传并返回新文件名称
	String fileName = FileUploadUtils.upload(filePath, file);
	fileInfo.setFilePath(fileName);
	return toAjax(sysFileInfoService.insertSysFileInfo(fileInfo));
}

4、上传成功后需要预览可以对该属性格式化处理

{
	field : 'filePath',
	title: '文件预览',
	formatter: function(value, row, index) {
		return $.table.imageView(value);
	}
},

如需对文件格式控制,设置application.yml中的multipart属性

# 文件上传
servlet:
  multipart:
    # 单个文件大小
    max-file-size: 10MB
    # 设置总上传的文件大小
    max-request-size: 20MB

注意:如果只是单纯的上传一张图片没有其他参数可以使用通用方法 /common/upload 请求处理方法 com.ruoyi.web.controller.common.CommonController

下载实现流程

1、参考示例代码。

function downloadFile(value) {
  window.location.href = ctx + "common/download/resource?resource=" + value;
}

2、参考Controller下载方法

/**
 * 本地资源通用下载
 */
@GetMapping("/common/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
		throws Exception
{
	// 本地资源路径
	String localPath = Global.getProfile();
	// 数据库资源地址
	String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
	// 下载名称
	String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
	response.setCharacterEncoding("utf-8");
	response.setContentType("multipart/form-data");
	response.setHeader("Content-Disposition",
			"attachment;fileName=" + FileUtils.setFileDownloadHeader(request, downloadName));
	FileUtils.writeBytes(downloadPath, response.getOutputStream());
}

权限注解

@RequestRoles

@RequiresRoles注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数

参数类型描述
valueString[]角色列表
logicalLogical角色之间的判断关系,默认为Logical.AND

示例1: 以下代码表示必须拥有admin角色才可访问

@RequiresRoles("admin")
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

示例2: 以下代码表示必须拥有admin和common角色才可访问

@RequiresRoles({"admin", "common"})
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

示例3: 以下代码表示需要拥有admin或common角色才可访问

@RequiresRoles(value = {"admin", "common"}, logical = Logical.OR)
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

@RequiresPermissions

@RequiresPermissions注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数

参数类型描述
valueString[]权限列表
logicalLogical权限之间的判断关系,默认为Logical.AND

示例1: 以下代码表示必须拥有system:user:add权限才可访问

@RequiresPermissions("system:user:add")
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

示例2: 以下代码表示必须拥有system:user:add和system:user:edit权限才可访问

@RequiresPermissions({"system:user:add", "system:user:edit"})
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

示例3: 以下代码表示需要拥有system:user:add或system:user:edit权限才可访问

@RequiresPermissions(value = {"system:user:add", "system:user:edit"}, logical = Logical.OR)
public AjaxResult save(...)
{
    return AjaxResult.success(...);
}

提示

Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关) RequiresRoles、RequiresPermissions、RequiresAuthentication、RequiresUser、RequiresGuest。 例如:你同时声明了RequiresRoles和RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。

编程式判断资源访问权限

示例: 编程式判断资源访问权限

Subject subject = ShiroUtils.getSubject();
subject.isPermitted(permission)        // 验证是否有资源权限
subject.isPermittedAll(permissions)    // 验证是否有资源权限(列表)

// 例如:
if (subject.isPermitted("sys:user:edit"))
{
    System.out.println("当前用户有编辑用户权限");
}

示例: 编程式判断角色访问权限

Subject subject = ShiroUtils.getSubject();
subject.hasRole(role)        // 验证是否有角色权限
subject.hasRoles(roles)      // 验证是否有角色权限(列表)

// 例如:
if (subject.hasRole("admin"))
{
    System.out.println("当前用户有admin角色权限");
}

基于URI判断用户访问权限

示例: 基于URI判断用户访问权限

// 验证是否有资源权限
filterChainDefinitionMap.put("/system/user/add", "perms[system:user:add]");
filterChainDefinitionMap.put("/system/user/**", "perms[system:user:*]");
// 验证是否有资源权限(列表),相当于isPermitedAll()方法。
filterChainDefinitionMap.put("/system/user/**", "perms[system:user:add,system:user:edit]");

示例: 基于URI判断角色访问权限

// 验证是否有角色权限
filterChainDefinitionMap.put("/system/user/**", "roles[admin]");
// 验证是否有角色权限(列表),相当于hasAllRoles()方法。
filterChainDefinitionMap.put("/system/user/**", "roles[admin,common]");

URI通配符

?  :匹配一个字符。
*  :匹配零个或多个字符串。
** :匹配路径中的零个或多个路径。

事务处理

新建的Spring Boot项目中,一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbc或spring-boot-starter-data-jpa的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。 所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

提示

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

例如用户新增需要插入用户表、用户与岗位关联表、用户与角色关联表,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作, 这样可以防止出现脏数据,就可以使用事务让它实现回退。 做法非常简单,我们只需要在方法或类添加@Transactional注解即可。

@Transactional
public int insertUser(User user)
{
	// 新增用户信息
	int rows = userMapper.insertUser(user);
	// 新增用户岗位关联
	insertUserPost(user);
	// 新增用户与角色管理
	insertUserRole(user);
	return rows;
}
  • 常见坑点1:遇到检查异常时,事务开启,也无法回滚。 例如下面这段代码,用户依旧增加成功,并没有因为后面遇到检查异常而回滚!!
@Transactional
public int insertUser(User user) throws Exception
{
	// 新增用户信息
	int rows = userMapper.insertUser(user);
	// 新增用户岗位关联
	insertUserPost(user);
	// 新增用户与角色管理
	insertUserRole(user);
	// 模拟抛出SQLException异常
	boolean flag = true;
	if (flag)
	{
		throw new SQLException("发生异常了..");
	}
	return rows;
}

原因分析:因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对检查异常进行事务回滚,可以在@Transactional注解里使用 rollbackFor属性明确指定异常。 例如下面这样,就可以正常回滚:

@Transactional(rollbackFor = Exception.class)
public int insertUser(User user) throws Exception
{
	// 新增用户信息
	int rows = userMapper.insertUser(user);
	// 新增用户岗位关联
	insertUserPost(user);
	// 新增用户与角色管理
	insertUserRole(user);
	// 模拟抛出SQLException异常
	boolean flag = true;
	if (flag)
	{
		throw new SQLException("发生异常了..");
	}
	return rows;
}
  • 常见坑点2: 在业务层捕捉异常后,发现事务不生效。 这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。 例如:下面这段代码直接导致用户新增的事务回滚没有生效。
@Transactional
public int insertUser(User user) throws Exception
{
	// 新增用户信息
	int rows = userMapper.insertUser(user);
	// 新增用户岗位关联
	insertUserPost(user);
	// 新增用户与角色管理
	insertUserRole(user);
	// 模拟抛出SQLException异常
	boolean flag = true;
	if (flag)
	{
		try
		{
			// 谨慎:尽量不要在业务层捕捉异常并处理
			throw new SQLException("发生异常了..");
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
	return rows;
}

推荐做法:在业务层统一抛出异常,然后在控制层统一处理。

@Transactional
public int insertUser(User user) throws Exception
{
	// 新增用户信息
	int rows = userMapper.insertUser(user);
	// 新增用户岗位关联
	insertUserPost(user);
	// 新增用户与角色管理
	insertUserRole(user);
	// 模拟抛出SQLException异常
	boolean flag = true;
	if (flag)
	{
		throw new RuntimeException("发生异常了..");
	}
	return rows;
}

注意事项

  • 同一个事务下是无法切换数据源
  • 禁止父方法使用@Transactional创建事务,子方法使用@DataSource切换数据源
  • 正确用法: 子方法单独创建事务或父方法使用@Transactional(propagation = Propagation.REQUIRES_NEW)为所有子方法创建新事务

Transactional注解的常用属性表:

属性说明
propagation事务的传播行为,默认值为 REQUIRED。
isolation事务的隔离度,默认值采用 DEFAULT
timeout事务的超时时间,默认值为-1,不超时。如果设置了超时时间(单位秒),那么如果超过该时间限制了但事务还没有完成,则自动回滚事务。
read-only指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。{xxx1.class, xxx2.class,……}
noRollbackFor抛出 no-rollback-for 指定的异常类型,不回滚事务。{xxx1.class, xxx2.class,……}
....

提示

事务的传播机制是指如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。 即:在执行一个@Transactinal注解标注的方法时,开启了事务;当该方法还在执行中时,另一个人也触发了该方法;那么此时怎么算事务呢,这时就可以通过事务的传播机制来指定处理方式。

TransactionDefinition传播行为的常量:

常量含义
TransactionDefinition.PROPAGATION_REQUIRED如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
TransactionDefinition.PROPAGATION_REQUIRES_NEW创建一个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_SUPPORTS如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER以非事务方式运行,如果当前存在事务,则抛出异常。
TransactionDefinition.PROPAGATION_MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
TransactionDefinition.PROPAGATION_NESTED如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIR

异常处理

通常一个web框架中,有大量需要处理的异常。比如业务异常,权限不足等等。前端通过弹出提示信息的方式告诉用户出了什么错误。 通常情况下我们用try.....catch....对异常进行捕捉处理,但是在实际项目中对业务模块进行异常捕捉,会造成代码重复和繁杂, 我们希望代码中只有业务相关的操作,所有的异常我们单独设立一个类来处理它。全局异常就是对框架所有异常进行统一管理。 我们在可能发生异常的方法里throw抛给控制器。然后由全局异常处理器对异常进行统一处理。 如此,我们的Controller中的方法就可以很简洁了。

所谓全局异常处理器就是使用@ControllerAdvice注解。示例如下:

1、统一返回实体定义

package com.ruoyi.common.core.domain;

import java.util.HashMap;

/**
 * 操作消息提醒
 *
 * @author ruoyi
 */
public class AjaxResult extends HashMap<String, Object>
{
    private static final long serialVersionUID = 1L;

    /**
     * 返回错误消息
     *
     * @param code 错误码
     * @param msg 内容
     * @return 错误消息
     */
    public static AjaxResult error(String msg)
    {
        AjaxResult json = new AjaxResult();
        json.put("msg", msg);
        json.put("code", 500);
        return json;
    }

    /**
     * 返回成功消息
     *
     * @param msg 内容
     * @return 成功消息
     */
    public static AjaxResult success(String msg)
    {
        AjaxResult json = new AjaxResult();
        json.put("msg", msg);
        json.put("code", 0);
        return json;
    }
}

2、定义登录异常定义

package com.ruoyi.common.exception;

/**
 * 登录异常
 *
 * @author ruoyi
 */
public class LoginException extends RuntimeException
{
    private static final long serialVersionUID = 1L;

    protected final String message;

    public LoginException(String message)
    {
        this.message = message;
    }

    @Override
    public String getMessage()
    {
        return message;
    }
}

3、基于@ControllerAdvice注解的Controller层的全局异常统一处理

package com.ruoyi.framework.web.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.LoginException;

/**
 * 全局异常处理器
 *
 * @author ruoyi
 */
@RestControllerAdvice
public class GlobalExceptionHandler
{
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

	/**
     * 登录异常
     */
    @ExceptionHandler(LoginException.class)
    public AjaxResult loginException(LoginException e)
    {
        log.error(e.getMessage(), e);
        return AjaxResult.error(e.getMessage());
    }
}

4、测试访问请求

@Controller
public class SysIndexController
{
    /**
     * 首页方法
     */
    @GetMapping("/index")
    public String index(ModelMap mmap)
    {
        /**
         * 模拟用户未登录,抛出业务逻辑异常
         */
        SysUser user = ShiroUtils.getSysUser();
        if (StringUtils.isNull(user))
		{
            throw new LoginException("用户未登录,无法访问请求。");
        }
		mmap.put("user", user);
        return "index";
    }
}

根据上面代码含义,当我们未登录访问/index时就会发生LoginException业务逻辑异常,按照我们之前的全局异常配置以及统一返回实体实例化,访问后会出现AjaxResult格式JSON数据, 下面我们运行项目访问查看效果。 界面输出内容如下所示:

{
  "msg": "用户未登录,无法访问请求。",
  "code": 500
}

对于一些特殊情况,如接口需要返回json,页面请求返回html可以使用如下方法:

@ExceptionHandler(LoginException.class)
public Object loginException(HttpServletRequest request, LoginException e)
{
	log.error(e.getMessage(), e);

	if (ServletUtils.isAjaxRequest(request))
	{
		return AjaxResult.error(e.getMessage());
	}
	else
	{
		return new ModelAndView("/error/500");
	}
}

若依系统的全局异常处理器GlobalExceptionHandler 注意:如果全部异常处理返回json,那么可以使用@RestControllerAdvice代替@ControllerAdvice,这样在方法上就可以不需要添加@ResponseBody。

无法捕获异常?

如果您的异常无法捕获,您可以从以下几个方面着手检查

异常是否已被处理,即抛出异常后被catch,打印了日志或抛出了其它异常 异常是否非Controller抛出,即在拦截器或过滤器中出现的异常

参数验证

注解参数说明

注解名称功能
@Xss检查该字段是否存在跨站脚本工具
@Null检查该字段为空
@NotNull不能为null
@NotBlank不能为空,常用于检查空字符串
@NotEmpty不能为空,多用于检测list是否size是0
@Max该字段的值只能小于或等于该值
@Min该字段的值只能大于或等于该值
@Past检查该字段的日期是在过去
@Future检查该字段的日期是否是属于将来的日期
@Email检查是否是一个有效的email地址
@Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
@Range(min=,max=,message=)被注释的元素必须在合适的范围内
@Size(min=, max=)检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等
@Length(min=,max=)检查所属的字段的长度是否在min和max之间,只能用于字符串
@AssertTrue用于boolean字段,该字段只能为true
@AssertFalse该字段的值只能为false

数据校验说明

1、基础使用 因为spring boot已经引入了基础包,所以直接使用就可以了。首先在controller上声明@Validated需要对数据进行校验。

public AjaxResult add(@Validated @RequestBody SysUser user)
{
    .....
}

2、然后在对应字段Get方法加上参数校验注解,如果不符合验证要求,则会以message的信息为准,返回给前端。

@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
public String getNickName()
{
	return nickName;
}

@NotBlank(message = "用户账号不能为空")
@Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
public String getUserName()
{
	return userName;
}

@Email(message = "邮箱格式不正确")
@Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
public String getEmail()
{
	return email;
}

@Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
public String getPhonenumber()
{
	return phonenumber;
}

也可以直接放在字段上面声明。

@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
private String nickName;

自定义注解校验

使用原生的@Validated进行参数校验时,都是特定的注解去校验(例如字段长度、大小、不为空等),我们也可以用自定义的注解去进行校验,例如项目中的@Xss注解。

1、新增Xss注解,设置自定义校验器XssValidator.class

/**
 * 自定义xss校验注解
 *
 * @author ruoyi
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER })
@Constraint(validatedBy = { XssValidator.class })
public @interface Xss
{
    String message()

    default "不允许任何脚本运行";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2、自定义Xss校验器,实现ConstraintValidator接口。

/**
 * 自定义xss校验注解实现
 *
 * @author ruoyi
 */
public class XssValidator implements ConstraintValidator<Xss, String>
{
    private final String HTML_PATTERN = "<(\\S*?)[^>]*>.*?|<.*? />";

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext)
    {
        return !containsHtml(value);
    }

    public boolean containsHtml(String value)
    {
        Pattern pattern = Pattern.compile(HTML_PATTERN);
        Matcher matcher = pattern.matcher(value);
        return matcher.matches();
    }
}

3、实体类使用自定义的@Xss注解

@Xss(message = "登录账号不能包含脚本字符")
@NotBlank(message = "登录账号不能为空")
@Size(min = 0, max = 30, message = "登录账号长度不能超过30个字符")
public String getLoginName()
{
	return loginName;
}

此时在去保存会进行验证,如果不符合规则的字符(例如<script>alert(1);</script>)会提示登录账号不能包含脚本字符,代表限制成功。

如果是在方法里面校验整个实体,参考示例。

@Autowired
protected Validator validator;

public void importUser(SysUser user)
{
    BeanValidators.validateWithException(validator, user);
}

自定义分组校验

有时候我们为了在使用实体类的情况下更好的区分出新增、修改和其他操作验证的不同,可以通过groups属性设置。使用方式如下

新增类接口,用于标识出不同的操作类型

public interface Add
{
}

public interface Edit
{
}

Controller.java

// 新增
public AjaxResult addSave(@Validated(Add.class) @RequestBody Xxxx xxxx)
{
    return success(xxxx);
}

// 编辑
public AjaxResult editSave(@Validated(Edit.class) @RequestBody Xxxx xxxx)
{
    return success(xxxx);
}

Model.java

// 仅在新增时验证
@NotNull(message = "不能为空", groups = {Add.class})
private String xxxx;

// 在新增和修改时验证
@NotBlank(message = "不能为空", groups = {Add.class, Edit.class})
private String xxxx;

提示

如果你有更多操作类型,也可以自定义类统一管理,使用方式就变成了Type.Add、Type.Edit、Type.Xxxx等。

package com.eva.core.constants;

/**
 * 操作类型
 */
public interface Type
{
    interface Add {}

    interface Edit {}

    interface Xxxx {}
}

数据脱敏

接口在返回一些敏感或隐私数据时,是需要进行脱敏处理,通常的手段是使用 * 隐藏一部分数据。例如:

类型原始数据脱敏数据
姓名若依若*
密码123456**
身份证4306021988121366664306**********6666
手机号15888888888158****8888
电子邮箱ruoyi@163.comr****@qq.com
银行卡号6211222200008888666*********8666
车牌号码湘F88888湘F8***8

基于Jackson拓展,只需要在字段上添加脱敏注解,即可实现对该字段进行脱敏。

在字段上添加脱敏注解。如下所示:

public class User extends BaseEntity
{
    @Sensitive(desensitizedType = DesensitizedType.PHONE)
    private String phonenumber;
}

所有类型如下:

类型原始数据脱敏数据
USERNAME若依若*
PASSWORD123456**
ID_CARD4306021988121366664306**********6666
PHONE15888888888158****8888
EMAILruoyi@163.comr****@qq.com
BANK_CARD6211222200008888666*********8666
CAR_LICENSE湘F88888湘F8***8

提示

其他的脱敏策略根据自己的需求在DesensitizedType.java去定义,可以是方法,也可以是正则表达式。

系统日志

注解参数说明

参数类型默认值描述
titleString空操作模块
businessTypeBusinessTypeOTHER操作功能(OTHER其他、INSERT新增、UPDATE修改、DELETE删除、GRANT授权、EXPORT导出、IMPORT导入、FORCE强退、GENCODE生成代码、CLEAN清空数据)
operatorTypeOperatorTypeMANAGE操作人类别(OTHER其他、MANAGE后台用户、MOBILE手机端用户)
isSaveRequestDatabooleantrue是否保存请求的参数
isSaveResponseDatabooleantrue是否保存响应的参数
excludeParamNamesString[]{}排除指定的请求参数

自定义操作功能

1、在BusinessType中新增业务操作类型如:

/**
 * 测试
 */
TEST,

2、在sys_dict_data字典数据表中初始化操作业务类型

insert into sys_dict_data values(25, 10, '测试',     '10', 'sys_oper_type',       '',   'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');

3、在Controller中使用注解

@Log(title = "测试标题", businessType = BusinessType.TEST)
public AjaxResult test(...)
{
    return success(...);
}

操作日志记录逻辑实现代码LogAspect.java(opens new window) 登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。

数据权限

在实际开发中,需要设置用户只能查看哪些部门的数据,这种情况一般称为数据权限。 例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。

提示

默认系统管理员admin拥有所有数据权限(userId=1),默认角色拥有所有数据权限(如不需要数据权限不用设置数据权限操作)

注解参数说明

参数类型默认值描述
deptAliasString空部门表的别名,用来给填充{}.dept_id字段
userAliasString空用户表的别名,用来填充(}.user_id字段
permissionString空权限字符(用于多个角色匹配符合要求的权限)默认根据权限注解@ss获取,多个权限用逗号分隔开来

permission说明: 一个user会拥有多个role,每个role都负责不同的业务场景,user可以通过permission精确控制哪些role在本次操作能生效 例子:当前user有两个role,分别为Role A(有权限字符串a)和Role B(有权限字符串b),分别是Dept A和Dept B部门下的角色。 而method接口能都返回这两个部门的数据,此时有个业务需求是user访问method这个接口希望仅返回Dept A的数据。 此时只需要控制permission中的权限让Role A有而Role B没有即可,即permission = "a"就能让Role A生效而Role B失效

数据权限使用

1、在(系统管理-角色管理)设置需要数据权限的角色 目前支持以下几种权限

  • 全部数据权限
  • 自定数据权限
  • 部门数据权限
  • 部门及以下数据权限
  • 仅本人数据权限

2、在需要数据权限控制方法上添加@DataScope注解,其中d和u用来表示表的别名

部门数据权限注解

@DataScope(deptAlias = "d")
public List<...> select(...)
{
    return mapper.select(...);
}

部门及用户权限注解

@DataScope(deptAlias = "d", userAlias = "u")
public List<...> select(...)
{
    return mapper.select(...);
}

3、在mybatis查询底部标签添加数据范围过滤

<select id="select" parameterType="..." resultMap="...Result">
    <include refid="select...Vo"/>
    <!-- 数据范围过滤 -->
    ${params.dataScope}
</select>

例如:用户管理(未过滤数据权限的情况):

select u.user_id, u.dept_id, u.login_name, u.user_name, u.email
	, u.phonenumber, u.password, u.sex, u.avatar, u.salt
	, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by
	, u.create_time, u.remark, d.dept_name
from sys_user u
	left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'

例如:用户管理(已过滤数据权限的情况):

select u.user_id, u.dept_id, u.login_name, u.user_name, u.email
	, u.phonenumber, u.password, u.sex, u.avatar, u.salt
	, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by
	, u.create_time, u.remark, d.dept_name
from sys_user u
	left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
	and u.dept_id in (
		select dept_id
		from sys_role_dept
		where role_id = 2
	)

结果很明显,我们多了如下语句。通过角色部门表(sys_role_dept)完成了数据权限过滤

and u.dept_id in (
	select dept_id
	from sys_role_dept
	where role_id = 2
)

逻辑实现代码 com.ruoyi.framework.aspectj.DataScopeAspect

提示

仅实体继承BaseEntity才会进行处理,SQL语句会存放到BaseEntity对象中的params属性中,然后在xml中通过${params.dataScope}获取拼接后的语句。 PS:如果是自己的业务表需要实现数据权限,需要有dept_id和user_id这两个字段。

多数据源

注解参数说明

参数类型默认值描述
valueDataSourceTypeDataSourceType.MASTER主库

多数据源使用

1、在application-druid.yml配置从库数据源

# 从库数据源
slave:
	# 从数据源开关/默认关闭
	enabled: true
	url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
	username: root
	password: password

2、在DataSourceType类添加数据源枚举

/**
 * 从库
 */
SLAVE

3、在DruidConfig配置读取数据源

@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties)
{
	DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
	dataSource.setKeepAlive(true); // 自动保活,防止从数据源连接超时
	return druidProperties.dataSource(dataSource);
}

4、在DruidConfig类dataSource方法添加数据源

setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");

5、在需要使用多数据源方法或类上添加@DataSource注解,其中value用来表示数据源

@DataSource(value = DataSourceType.SLAVE)
public List<SysUser> selectUserList(SysUser user)
{
	return userMapper.selectUserList(user);
}
@Service
@DataSource(value = DataSourceType.SLAVE)
public class SysUserServiceImpl

手动切换数据源

在需要切换数据源的方法中使用DynamicDataSourceContextHolder类实现手动切换,使用方法如下:

public List<SysUser> selectUserList(SysUser user)
{
	DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name());
	List<SysUser> userList = userMapper.selectUserList(user);
	DynamicDataSourceContextHolder.clearDataSourceType();
	return userList;
}

逻辑实现代码 com.ruoyi.framework.aspectj.DataSourceAspect

注意:目前配置了一个从库,默认关闭状态。如果不需要多数据源不用做任何配置。 另外可新增多个从库。支持不同数据源(Mysql、Oracle、SQLServer)

提示

如果有Service方法内多个注解无效的情况使用内部方法调用SpringUtils.getAopProxy(this).xxxxxx(xxxx);

代码生成

默认配置

单应用在resources目录下的application.yml,多模块ruoyi-generator中的resources目录下的generator.yml,可以自己根据实际情况调整默认配置。

# 代码生成
gen:
  # 开发者姓名,生成到类注释上
  author: ruoyi
  # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
  packageName: com.ruoyi.system
  # 自动去除表前缀,默认是false
  autoRemovePre: false
  # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
  tablePrefix: sys_

单表结构

新建数据库表结构(单表)

drop table if exists sys_student;
create table sys_student (
  student_id           int(11)         auto_increment    comment '编号',
  student_name         varchar(30)     default ''        comment '学生名称',
  student_age          int(3)          default null      comment '年龄',
  student_hobby        varchar(30)     default ''        comment '爱好(0代码 1音乐 2电影)',
  student_sex          char(1)         default '0'       comment '性别(0男 1女 2未知)',
  student_status       char(1)         default '0'       comment '状态(0正常 1停用)',
  student_birthday     datetime                          comment '生日',
  primary key (student_id)
) engine=innodb auto_increment=1 comment = '学生信息表';

树表结构

新建数据库表结构(树表)

drop table if exists sys_product;
create table sys_product (
  product_id        bigint(20)      not null auto_increment    comment '产品id',
  parent_id         bigint(20)      default 0                  comment '父产品id',
  product_name      varchar(30)     default ''                 comment '产品名称',
  order_num         int(4)          default 0                  comment '显示顺序',
  status            char(1)         default '0'                comment '产品状态(0正常 1停用)',
  primary key (product_id)
) engine=innodb auto_increment=1 comment = '产品表';

主子表结构

新建数据库表结构(主子表)

-- ----------------------------
-- 客户表
-- ----------------------------
drop table if exists sys_customer;
create table sys_customer (
  customer_id           bigint(20)      not null auto_increment    comment '客户id',
  customer_name         varchar(30)     default ''                 comment '客户姓名',
  phonenumber           varchar(11)     default ''                 comment '手机号码',
  sex                   varchar(20)     default null               comment '客户性别',
  birthday              datetime                                   comment '客户生日',
  remark                varchar(500)    default null               comment '客户描述',
  primary key (customer_id)
) engine=innodb auto_increment=1 comment = '客户表';


-- ----------------------------
-- 商品表
-- ----------------------------
drop table if exists sys_goods;
create table sys_goods (
  goods_id           bigint(20)      not null auto_increment    comment '商品id',
  customer_id        bigint(20)      not null                   comment '客户id',
  name               varchar(30)     default ''                 comment '商品名称',
  weight             int(5)          default null               comment '商品重量',
  price              decimal(6,2)    default null               comment '商品价格',
  date               datetime                                   comment '商品时间',
  type               char(1)         default null               comment '商品种类',
  primary key (goods_id)
) engine=innodb auto_increment=1 comment = '商品表';

代码生成使用

1、登录系统(系统工具 -> 代码生成 -> 导入对应表)

2、代码生成列表中找到需要表(可预览、编辑、同步、删除生成配置)

3、点击生成代码会得到一个ruoyi.zip执行sql文件,按照包内目录结构复制到自己的项目中即可

代码生成支持编辑、预览、同步

预览:对生成的代码提前预览,防止出现一些不符合预期的情况。

同步:对原表的字段进行同步,包括新增、删除、修改的字段处理。

修改:对生成的代码基本信息、字段信息、生成信息做一系列的调整。

另外多模块所有代码生成的相关业务逻辑代码在ruoyi-generator模块,不需要可以自行删除模块。

定时任务

在实际项目开发中Web应用有一类不可缺少的,那就是定时任务。 定时任务的场景可以说非常广泛,比如某些视频网站,购买会员后,每天会给会员送成长值,每月会给会员送一些电影券; 比如在保证最终一致性的场景中,往往利用定时任务调度进行一些比对工作;比如一些定时需要生成的报表、邮件;比如一些需要定时清理数据的任务等。 所以我们提供方便友好的web界面,实现动态管理任务,可以达到动态控制定时任务启动、暂停、重启、删除、添加、修改等操作,极大地方便了开发过程。

提示

关于定时任务使用流程

1、后台添加定时任务处理类(支持Bean调用、Class类调用) Bean调用示例:需要添加对应Bean注解@Component或@Service。调用目标字符串:ryTask.ryParams('ry')Class类调用示例:添加类和方法指定包即可。调用目标字符串:com.ruoyi.quartz.task.RyTask.ryParams('ry')

/**
 * 定时任务调度测试
 *
 * @author ruoyi
 */
@Component("ryTask")
public class RyTask
{
    public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i)
    {
        System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
    }

    public void ryParams(String params)
    {
        System.out.println("执行有参方法:" + params);
    }

    public void ryNoParams()
    {
        System.out.println("执行无参方法");
    }
}

2、前端新建定时任务信息(系统监控 -> 定时任务) 任务名称:自定义,如:定时查询任务状态 任务分组:根据字典sys_job_group配置 调用目标字符串:设置后台任务方法名称参数 执行表达式:可查询官方cron表达式介绍 执行策略:定时任务自定义执行策略 并发执行:是否需要多个任务间同时执行 状态:是否启动定时任务 备注:定时任务描述信息

3、点击执行一次,测试定时任务是否正常及调度日志是否正确记录,如正常执行表示任务配置成功。

执行策略详解: 立即执行(所有misfire的任务会马上执行)打个比方,如果9点misfire了,在10:15系统恢复之后,9点,10点的misfire会马上执行 执行一次(会合并部分的misfire,正常执行下一个周期的任务)假设9,10的任务都misfire了,系统在10:15分起来了。只会执行一次misfire,下次正点执行。 放弃执行(所有的misfire不管,执行下一个周期的任务)

方法参数详解: 字符串(需要单引号''标识 如:ryTask.ryParams(’ry’)) 布尔类型(需要true false标识 如:ryTask.ryParams(true)) 长整型(需要L标识 如:ryTask.ryParams(2000L)) 浮点型(需要D标识 如:ryTask.ryParams(316.50D)) 整型(纯数字即可)

cron表达式语法: [秒] [分] [小时] [日] [月] [周] [年]

说明必填允许填写的值允许的通配符
秒是0-59, - * /
分是0-59, - * /
时是0-23, - * /
日是1-31, - * /
月是1-12 / JAN-DEC, - * ? / L W
周是1-7 or SUN-SAT, - * ? / L #
年是1970-2099, - * /

通配符说明: * 表示所有值。 例如:在分的字段上设置 _,表示每一分钟都会触发 ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体设置为 0 0 0 10 _ ? - 表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发 , 表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发 / 用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置’1/3’所示每月1号开始,每隔三天触发一次 L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五” W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“) # 序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周五.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同

常用表达式例子:

表达式说明
0 0 2 1 _ ? _表示在每月的1日的凌晨2点调整任务
0 15 10 ? * MON-FRI表示周一到周五每天上午10:15执行作业
0 15 10 ? 6L 2002-2006表示2002-2006年的每个月的最后一个星期五上午10:15执行作
0 0 10,14,16 * * ?每天上午10点,下午2点,4点
0 0/30 9-17 * * ?朝九晚五工作时间内每半小时
0 0 12 ? * WED表示每个星期三中午12点
0 0 12 * * ?每天中午12点触发
0 15 10 ? * *每天上午10:15触发
0 15 10 * * ?每天上午10:15触发
0 15 10 * _ ? _每天上午10:15触发
0 15 10 * * ? 20052005年的每天上午10:15触发
0 _ 14 _ * ?在每天下午2点到下午2:59期间的每1分钟触发
0 0/5 14 * * ?在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ?在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0-5 14 * * ?在每天下午2点到下午2:05期间的每1分钟触发
0 10,44 14 ? 3 WED每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI周一至周五的上午10:15触发
0 15 10 15 * ?每月15日上午10:15触发
0 15 10 L * ?每月最后一日的上午10:15触发
0 15 10 ? * 6L每月的最后一个星期五上午10:15触发
0 15 10 ? * 6L 2002-20052002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3每月的第三个星期五上午10:15触发

多模块所有定时任务的相关业务逻辑代码在ruoyi-quartz模块,可以自行调整或剔除

注意:不同数据源定时任务都有对应脚本,Oracle、Mysql已经有了,其他的可自行下载执行

系统接口

在现在的开发过程中还有很大一部分公司都是以口口相传的方式来进行前后端的联调,而接口文档很大一部分都只停留在了说说而已的地步,或者写了代码再写文档。 还有一点就是文档的修改,定义好的接口并不是一成不变的,可能在开发过程中文档修改不止一次的变化,这个时候就会很难受了。 只要不是强制性要求,没人会愿意写这东西,而且在写的过程中,一个字母的错误就会导致联调时候的很大麻烦,但是通过Swagger,我们可以省略了这一步,而且文档出错率近乎于零, 只要你在写代码的时候,稍加几个注解,文档自动生成。

1、在控制层Controller中添加注解来描述接口信息如:

@Api("参数配置")
@Controller
@RequestMapping("/system/config")
public class ConfigController

2、在方法中配置接口的标题信息

@ApiOperation("查询参数列表")
@ResponseBody
public TableDataInfo list(Config config)
{
	startPage();
	List<Config> list = configService.selectConfigList(config);
	return getDataTable(list);
}

3、在系统工具-系统接口测试相关接口

注意:SwaggerConfig可以指定根据注解或者包名扫描具体的API

API详细说明

作用范围API使用位置
协议集描述@Api用于controller类上
对象属性@ApiModelProperty用在出入参数对象的字段上
协议描述@ApiOperation用在controller的方法上
Response集@ApiResponses用在controller的方法上
Response@ApiResponse用在 @ApiResponses里边
非对象参数集@ApiImplicitParams用在controller的方法上
非对象参数描述@ApiImplicitParam用在@ApiImplicitParams的方法里边
描述返回对象的意义@ApiModel用在返回对象类上

api标记,用在类上,说明该类的作用。可以标记一个Controller类做为Swagger文档资源,使用方式:

@Api(value = "/user", description = "用户管理")

与Controller注解并列使用。 属性配置:

属性名称备注
valueurl的路径值
tags如果设置这个值、value的值会被覆盖
description对api资源的描述
basePath基本路径可以不配置
position如果配置多个Api 想改变显示的顺序位置
producesFor example, "application/json, application/xml"
consumesFor example, "application/json, application/xml"
protocolsPossible values: http, https, ws, wss.
authorizations高级特性认证时配置
hidden配置为true 将在文档中隐藏

ApiOperation标记,用在方法上,说明方法的作用,每一个url资源的定义,使用方式:

@ApiOperation("获取用户信息")

与Controller中的方法并列使用,属性配置:

属性名称备注
valueurl的路径值
tags如果设置这个值、value的值会被覆盖
description对api资源的描述
basePath基本路径可以不配置
position如果配置多个Api 想改变显示的顺序位置
producesFor example, "application/json, application/xml"
consumesFor example, "application/json, application/xml"
protocolsPossible values: http, https, ws, wss.
authorizations高级特性认证时配置
hidden配置为true将在文档中隐藏
response返回的对象
responseContainer这些对象是有效的 "List", "Set" or "Map".,其他无效
httpMethod"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS" and "PATCH"
codehttp的状态码 默认 200
extensions扩展属性

ApiParam标记,请求属性,使用方式:

public TableDataInfo list(@ApiParam(value = "查询用户列表", required = true)User user)

与Controller中的方法并列使用,属性配置:

属性名称备注
name属性名称
value属性值
defaultValue默认属性值
allowableValues可以不配置
required是否属性必填
access不过多描述
allowMultiple默认为false
hidden隐藏该属性
example举例子

ApiResponse标记,响应配置,使用方式:

@ApiResponse(code = 400, message = "查询用户失败")

与Controller中的方法并列使用,属性配置:

属性名称备注
codehttp的状态码
message描述
response默认响应类 Void
reference参考ApiOperation中配置
responseHeaders参考 ResponseHeader 属性配置说明
responseContainer参考ApiOperation中配置

ApiResponses标记,响应集配置,使用方式:

@ApiResponses({ @ApiResponse(code = 400, message = "无效的用户") })

与Controller中的方法并列使用,属性配置:

属性名称备注
value多个ApiResponse配置

ResponseHeader标记,响应头设置,使用方法

@ResponseHeader(name="head",description="响应头设计")

与Controller中的方法并列使用,属性配置:

属性名称备注
name响应头名称
description描述
response默认响应类 void
responseContainer参考ApiOperation中配置

防重复提交

在接口方法上添加@RepeatSubmit注解即可,注解参数说明:

参数类型默认值描述
intervalint5000间隔时间(ms),小于此时间视为重复提交
messageString不允许重复提交,请稍后再试提示消息

示例1:采用默认参数

@RepeatSubmit
public AjaxResult addSave(...)
{
    return success(...);
}

示例2:指定防重复时间和错误消息

@RepeatSubmit(interval = 1000, message = "请求过于频繁")
public AjaxResult addSave(...)
{
    return success(...);
}

国际化支持

在我们开发WEB项目的时候,项目可能涉及到在国外部署或者应用,也有可能会有国外的用户对项目进行访问,那么在这种项目中, 为客户展现的页面或者操作的信息就需要使用不同的语言,这就是我们所说的项目国际化。 目前项目已经支持多语言国际化,接下来我们介绍如何使用。

后台国际化流程

1、修改I18nConfig设置默认语言,如默认中文:

// 默认语言,英文可以设置Locale.US
slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);

2、修改配置application.yml中的basename国际化文件,默认是i18n路径下messages文件 (比如现在国际化文件是xx_zh_CN.properties、xx_en_US.properties,那么basename配置应为是i18n/xx

spring:
  # 资源信息
  messages:
    # 国际化资源文件路径
    basename: static/i18n/messages

3、i18n目录文件下定义资源文件 美式英语 messages_en_US.properties

user.login.username=User name
user.login.password=Password
user.login.code=Security code
user.login.remember=Remember me
user.login.submit=Sign In

中文简体 messages_zh_CN.properties

user.login.username=用户名
user.login.password=密码
user.login.code=验证码
user.login.remember=记住我
user.login.submit=登录

4、java代码使用MessageUtils获取国际化

MessageUtils.message("user.login.username")
MessageUtils.message("user.login.password")
MessageUtils.message("user.login.code")
MessageUtils.message("user.login.remember")
MessageUtils.message("user.login.submit")

前端国际化流程

1、html使用国际化#{资源文件key}

<form id="signupForm">
  <h4 class="no-margins">登录:</h4>
  <p class="m-t-md">你若不离不弃,我必生死相依</p>
  <input
    type="text"
    name="username"
    class="form-control uname"
    th:placeholder="#{user.login.username}"
  />
  <input
    type="password"
    name="password"
    class="form-control pword"
    th:placeholder="#{user.login.password}"
  />
  <div class="row m-t" th:if="${captchaEnabled==true}">
    <div class="col-xs-6">
      <input
        type="text"
        name="validateCode"
        class="form-control code"
        th:placeholder="#{user.login.code}"
        maxlength="5"
        autocomplete="off"
      />
    </div>
    <div class="col-xs-6">
      <a href="javascript:void(0);" title="点击更换验证码">
        <img
          th:src="@{captcha/captchaImage(type=${captchaType})}"
          class="imgcode"
          width="85%"
        />
      </a>
    </div>
  </div>
  <div
    class="checkbox-custom"
    th:classappend="${captchaEnabled==false} ? 'm-t'"
  >
    <input type="checkbox" id="rememberme" name="rememberme" />
    <label for="rememberme" th:text="#{user.login.remember}">记住我</label>
  </div>
  <button
    class="btn btn-success btn-block"
    id="btnSubmit"
    data-loading="正在验证登录,请稍后..."
    th:text="#{user.login.submit}"
  >
    登录
  </button>
</form>

2、js使用国际化 首先在文件引入jquery-i18n-properties依赖,然后在初始化后即可通过JS函数获取对应国际化文件的内容。

<!--jQuery国际化插件-->
<script
  src="../static/js/jquery.i18n.properties.min.js"
  th:src="@{/js/jquery.i18n.properties.min.js}"
></script>

<script th:inline="javascript">
  //获取应用路径
  var ROOT = [[${#servletContext.contextPath}]];

  //获取默认语言
  var LANG_COUNTRY = [[${#locale.language+'_'+#locale.country}]];

  //初始化i18n插件
  $.i18n.properties({
  	path: ROOT + '/i18n/',//这里表示访问路径
  	name: 'messages',//文件名开头
  	language: LANG_COUNTRY,//文件名语言 例如en_US
  	mode: 'map'//默认值
  });

  //初始化i18n函数
  function i18n(msgKey) {
  	try {
  		return $.i18n.prop(msgKey);
  	} catch (e) {
  		return msgKey;
  	}
  }

  //获取国际化翻译值
  console.log(i18n('user.login.username'));
  console.log(i18n('user.login.password'));
  console.log(i18n('user.login.code'));
  console.log(i18n('user.login.remember'));
  console.log(i18n('user.login.submit'));
</script>

3、界面定义切换语言

<a href="?lang=en_US"> 英语 </a> <a href="?lang=zh_CN"> 中文 </a>

4、使用html使用模板引擎语法直接获取

标签
<div th:text="#{user.login.username}"></div>
文本
<div>[[#{user.login.username}]]</div>

新建子模块

Maven多模块下新建子模块流程案例。

1、新建业务模块目录,例如:ruoyi-test。

2、在ruoyi-test业务模块下新建pom.xml文件以及src\main\java,src\main\resources目录。

<?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>ruoyi</artifactId>
        <groupId>com.ruoyi</groupId>
        <version>x.x.x</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>ruoyi-test</artifactId>

    <description>
        test系统模块
    </description>

    <dependencies>

        <!-- 通用工具-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common</artifactId>
        </dependency>

    </dependencies>

</project>

3、根目录pom.xml依赖声明节点dependencies中添加依赖

<!-- 测试模块-->
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>ruoyi-test</artifactId>
    <version>${ruoyi.version}</version>
</dependency>

4、根目录pom.xml模块节点modules添加业务模块

<module>ruoyi-test</module>

5、ruoyi-admin目录pom.xml添加模块依赖

<!-- 测试模块-->
<dependency>
    <groupId>com.ruoyi</groupId>
    <artifactId>ruoyi-test</artifactId>
</dependency>

6、测试模块

在ruoyi-test业务模块添加com.ruoyi.test包,新建TestService.java

public class TestService
{
    public String helloTest()
    {
        return "hello";
    }
}

在ruoyi-admin新建测试类,调用helloTest成功返回hello代表成功。

前端手册

前端组件

系统封装了一些常用的JS组件方法。

名称代码介绍
表格$.table表格封装处理
表格树$.treeTable表格树封装处理
表单$.form表单封装处理
弹出层$.modal弹出层封装处理
操作$.operate操作封装处理
校验$.validate校验封装处理
树插件$.tree树插件封装处理
通用方法$.common通用方法封装处理

通用方法

表格方法

方法参数介绍
$.table.init();options(选项参数)初始化表格参数
$.table.search();formId(表单ID, 表格ID, 追加数据)搜索-默认第一个form
$.table.exportExcel();formId(表单ID)导出-默认第一个form
$.table.importExcel();formId(表单ID)导入-默认importForm
$.table.importTemplate();formId(表单ID)模板下载
$.table.refresh();无刷新表格
$.table.selectColumns();column(查询列值)查询表格指定列值
$.table.selectFirstColumns();无查询表格首列值
$.table.destroy();tableId(表格ID)销毁表格-默认options.id
$.table.serialNumber();index(序号)序列号生成
$.table.dropdownToggle();value(内容)下拉按钮切换
$.table.imageView();value(内容), height, width, target(打开方式)图片预览
$.table.showColumn();column(列值), tablbeId(表格ID)显示表格指定列
$.table.hideColumn();column(列值), tablbeId(表格ID)隐藏表格指定列
$.table.showAllColumns();tableId(表格ID)显示所有表格列
$.table.hideAllColumns();tableId(表格ID)隐藏所有表格列
$.table.tooltip();value(内容), length(截取长度), target(显示方式)超出指定长度浮动提示
$.table.selectDictLabel();datas(字典列表), value(当前值)回显数据字典
$.treeTable.init();options(选项参数)初始化表格树参数
$.treeTable.search();formId(表单ID)搜索-默认第一个form
$.treeTable.refresh();无刷新表格树

表单方法

方法参数介绍
$.form.reset();formId(表单ID, 表格ID)表单重置
$.form.selectCheckeds();name(name名称)获取选中复选框项
$.form.selectSelects();name(id名称)获取选中下拉框项

弹层方法

方法参数介绍
$.modal.icontype(图标类型)显示图标
$.modal.msgcontent(内容), type(图标类型)消息提示
$.modal.msgError();content(内容)错误消息
$.modal.msgSuccess();content(内容)成功消息
$.modal.msgWarning();content(内容)警告消息
$.modal.alertcontent(内容), type(图标类型)消息提示
$.modal.alertError();content(内容)错误提示
$.modal.alertSuccess();content(内容)成功提示
$.modal.alertWarning();content(内容)警告提示
$.modal.msgReloadmsg(消息), type(图标类型)消息提示,重新加载页面
$.modal.msgSuccessReloadmsg(消息)消息提示成功并刷新父窗体
$.modal.close();无关闭窗体
$.modal.closeAll无关闭全部窗体
$.modal.confirm();content(内容), callBack(回调函数)确认窗体
$.modal.open();title, url, width, height, callBack(回调函数)弹出层指定宽度
$.modal.openOptions();options(选项参数)弹出层指定参数选项
$.modal.openFull();title, url, width, height弹出层全屏
$.modal.openTab();title(标题), url(地址)选卡页方式打开
$.modal.parentTab();title(标题), url(地址)选卡页同一页签打开
$.modal.closeTab();dataId(地址)关闭选项卡
$.modal.disable无禁用按钮
$.modal.enable无启用按钮
$.modal.loading();message(提示消息)打开遮罩层
$.modal.closeLoading();无关闭遮罩层
$.modal.reload();无重新加载

操作方法

方法参数介绍
$.operate.submit();url, type, dataType, data, callback(回调函数)提交数据
$.operate.post();url(地址), data(数据), callback(回调函数)post方式请求提交数据
$.operate.get();url(地址), callback(回调函数)get请求传输数据
$.operate.detail();id(数据ID)详细信息
$.operate.remove();id(数据ID)删除信息
$.operate.removeAll();无批量删除信息
$.operate.clean();无清空信息
$.operate.add();id(数据ID)添加信息
$.operate.addTab();id(数据ID)添加信息(选项卡方式)
$.operate.addFull();id(数据ID)添加信息 全屏
$.operate.addUrl();id(数据ID)添加访问地址
$.operate.edit();id(数据ID)修改信息
$.operate.editTab();id(数据ID)修改信息(选项卡方式)
$.operate.editFull();id(数据ID)修改信息 全屏
$.operate.editUrl();id(数据ID)修改访问地址
$.operate.save();url(地址), data(数据), callback(回调函数)保存信息
$.operate.saveModal();url(地址), data(数据), callback(回调函数)保存信息 弹出结果提示框
$.operate.saveTab();url(地址), data(数据), callback(回调函数)保存选项卡信息
$.operate.ajaxSuccess();result(返回结果)保存结果弹出msg刷新table表格
$.operate.saveReload();result(返回结果)保存结果重新加载页面
$.operate.successCallback();result(返回结果)成功回调执行事件(静默更新)
$.operate.successTabCallback();result(返回结果)选项卡成功回调执行事件(静默更新)
$.validate.unique();value(返回标识)判断返回标识是否唯一
$.validate.form();formId(表单ID)表单验证-默认第一个form
$.validate.reset();formId(表单ID)重置表单验证(清除提示信息)

通用方法

方法参数介绍
$.tree.init();options(选项参数)初始化树结构
$.tree.searchNode();无搜索节点
$.tree.selectByIdName();treeId, treeName, node根据Id和Name选中指定节点
$.tree.showAllNode();nodes(全部节点数据)显示所有节点
$.tree.hideAllNode();nodes(全部节点数据)隐藏所有节点
$.tree.showParent();treeNode(节点数据)显示所有父节点
$.tree.showChildren();treeNode(节点数据)显示所有孩子节点
$.tree.updateNodes();nodeList(全部节点数据)更新节点状态
$.tree.getCheckedNodes();column(列值)获取当前被勾选集合
$.tree.notAllowParents();_tree(树对象)不允许根父节点选择
$.tree.toggleSearch();无隐藏/显示搜索栏
$.tree.collapse();无树折叠
$.tree.expand();无树展开
$.common.isEmpty();value(值)判断字符串是否为空
$.common.isNotEmpty();value(值)判断一个字符串是否为非空串
$.common.nullToStr();value(值)空对象转字符串
$.common.visible();value(值)是否显示数据 为空默认为显示
$.common.trim();value(值)空格截取
$.common.equals();str(比较值1), that(比较值2)比较两个字符串(大小写敏感)
$.common.equalsIgnoreCase();str(比较值1), that(比较值2)比较两个字符串(大小写不敏感)
$.common.split();str(值), sep(分隔符), maxLen(最大长度)将字符串按指定字符分割
$.common.sprintf();str(值)字符串格式化(%s )
$.common.dateFormat();date(日期), format(日期格式)日期格式化(yyyy-MM-dd HH-mm-ss)
$.common.getItemField();item(数据), field(属性)获取节点数据,支持多层级访问
$.common.random();min(最小), max(最大)指定随机数返回
$.common.startWith();value(值), start(开始值)判断字符串是否是以start开头
$.common.endWith();value(值), end(结束值)判断字符串是否是以end结尾
$.common.uniqueFn();array(数组)数组去重
$.common.join();array(数组), separator(分隔符)数组中的所有元素放入一个字符串
$.common.formToJSON();formId(表单ID)获取form下所有的字段并转换为json对象
$.common.dictToSelect();datas(字典数据), value(值), name(属性名)数据字典转下拉框
$.common.getLength();obj(对象)获取obj对象长度
$.common.isMobile();无判断移动端
$.common.numValid();text(值)数字正则表达式,只能为0-9数字
$.common.enValid();text(值)英文正则表达式,只能为a-z和A-Z字母
$.common.enNumValid();text(值)英文、数字正则表达式,必须包含(字母,数字)
$.common.charValid();text(值)英文、数字、特殊字符正则表达式,必须包含(字母,数字,特殊字符!@#$%^&*()-=_+)

表格使用

表格组件基于bootstrap-table (opens new window)组件进行封装,轻松实现数据表格。

  • 表格初始化方法 $.table.init

表的各项

参数类型默认值描述
urlStringNull请求后台的URL
uniqueIdStringNull指定唯一列属性 配合删除/修改使用 未指定则使用表格行首列
createUrlStringNull新增URL 配合使用 $.operate.add(),$.operate.addTab()
updateUrlStringNull修改URL 配合使用 $.operate.edit(),$.operate.editTab()
removeUrlStringNull删除URL 配合使用 $.operate.remove()
exportUrlStringNull导出URL 配合使用 $.table.exportExcel()
importUrlStringNull导入URL 配合使用 $.table.importExcel()
detailUrlStringNull详细URL 配合使用 $.operate.detail()
cleanUrlStringNull清空URL 配合使用 $.operate.clean()
importTemplateUrlStringNull模板URL 配合使用 $.table.importTemplate()
heightStringundefined表格的高度
stripedStringfalse是否显示行间隔色
undefinedTextString'-'数据值为空时显示的内容
sortNameStringNull排序列名称
sortOrderStringNull排序方式 asc 或者 desc
paginationBooleantrue默认为true表格的底部工具栏会显示分页条,设为false不显示
paginationLoopBooleanfalse默认为true不启用分页条无限循环的功能
pageSizeint10每页的记录行数(*)
pageListArray[10, 25, 50]可供选择的每页的行数
idStringbootstrap-table表格ID属性
toolbarStringtoolbar表格工具栏ID属性
escapeBooleanfalse是否转义HTML字符串
firstLoadBooleantrue是否首次请求加载数据,对于数据较大可以配置false
showFooterBooleanfalse默认为false隐藏表尾,设为true显示
sidePaginationStringserverserver启用服务端分页client客户端分页
virtualScrollStringfalse是否启动虚拟滚动(大量数据纯展示时使用)
loadingFontSizeStringauto自定义加载文本的字体大小
searchBooleantrue默认为true显示搜索框功能,设为false隐藏
searchTextString''搜索框初始显示的内容,需要启用search设为true
showSearchBooleantrue默认为true显示检索信息,设为false隐藏
showPageGoBooleanfalse默认为false不显示跳转页,设为true显示
showRefreshBooleantrue默认为true显示刷新按钮,设为false隐藏
showColumnsBooleantrue默认为true显示某列下拉菜单,设为false隐藏
showToggleBooleantrue默认为true显示视图切换按钮,设为false隐藏
showExportBooleantrue默认为true显示导出文件按钮,设为false隐藏
showPrintBooleantrue默认为true显示打印页面按钮,设为false隐藏
showHeaderBooleantrue默认为true显示表头,设为false隐藏
showFullscreenBooleanfalse默认为false不全屏显示,设为true全屏显示
clickToSelectBooleanfalse默认为false不启用点击选中行,设为true启用
singleSelectBooleanfalse是否单选checkbox
mobileResponsiveBooleantrue是否支持移动端适配
cardViewBooleanfalse是否启用显示卡片视图
detailViewBooleanfalse是否启用显示细节视图
onCheckFunctiononCheck(row, $element)当选择此行时触发
onUncheckFunctiononUncheck(row, $element)当取消此行时触发
onCheckAllFunctiononCheckAll(rowsAfter, rowsBefore)当全选行时触发
onUncheckAllFunctiononUncheckAll(rowsAfter, rowsBefore)当取消全选行时触发
onClickRowFunctiononClickRow(row, $element)点击某行触发的事件
onDblClickRowFunctiononDblClickRow(row, $element)双击某行触发的事件
onClickCellFunctiononClickCell(field, value, row, $element)单击某格触发的事件
onDblClickCellFunctiononDblClickCell(field, value, row, $element)双击某格触发的事件
onEditableSaveFunctiononEditableSave(field, row, oldValue, $el)行内编辑保存的事件
onExpandRowFunctiononExpandRow(index, row, $detail)点击详细视图的事件
onPostBodyFunctiononPostBody(data)渲染完成后执行的事件
maintainSelectedBooleanfalse默认为false前端翻页时不保留所选行,设为true启用
rememberSelectedBooleanfalse默认为false不启用翻页记住前面的选择,设为true启用
fixedColumnsBooleanfalse默认为false禁用冻结列,设为true启用冻结列(左侧)
fixedNumberint0冻结列的个数,当fixedColumns设为true有效(左侧)
fixedRightNumberint0冻结列的个数,当fixedColumns设为true有效(右侧)
onReorderRowFunctiononReorderRow: function (data)当拖拽结束后处理函数
rowStyleFunctionrowStyle(row, index)改变某行的格式,需要两个参数:row行的数据index行的索引
footerStyleFunctionfooterStyle(column)通过自定义函数设置页脚样式
headerStyleFunctionheaderStyle(column)通过自定义函数设置标题样式
paramsArrayNull当请求数据时,你可以通过修改queryParams向服务器发送参数
columnsArrayNull默认空数组,在JS里面定义 参考列的各项(Column options )
dataArray[]默认空数组,被加载的数据
responseHandlerobjectresponseHandler(res)在加载服务器发送来的数据之前,处理数据的格式
onLoadSuccessobjectonLoadSuccess(data)当所有数据被加载时触发处理函数
exportOptionsobjectignoreColumn: [0, 8]前端导出设置ignoreColumn忽略列索引如[0,5,10]
exportDataTypeString'all'导出方式(默认all:导出所有数据;all:导出当前页的数据;selected:导出选中的数据)
exportTypesArray['csv', 'txt', 'doc', 'excel']导出文件类型 (json、xml、png、csv、txt、sql、doc、excel、xlsx、powerpoint、pdf)
printPageBuilderFunctionprintPageBuilder(table)自定义打印页面模板
detailFormatterFunction(index, row, element)detailView设为true,启用了显示detail view。用于格式化细节视图

列的各项

参数类型默认值描述
radioBooleanfalse默认false不显示radio(单选按钮),设为true则显示,radio宽度是固定的
checkboxBooleanfalse默认false不显示checkbox(复选框),设为true则显示,checkbox的每列宽度已固定
fieldStringNull是每列的字段名,不是表头所显示的名字,通过这个字段名可以给其赋值,相当于key,表内唯一
titleStringNull这个是表头所显示的名字,不唯一,如果你喜欢,可以把所有表头都设为相同的名字
titleTooltipStringtrue当悬浮在某控件上,出现提示 - 参考 Bootstrap 提示工具(Tooltip)插件
classbooleanfalse表格列样式
rowspanNumbertrue每格所占的行数
colspanNumbertrue每格所占的列数
alignStringtrue每格内数据的对齐方式,有:left(靠左)、right(靠右)、center(居中)
halignStringtruetable header(表头)的对齐方式,有:left(靠左)、right(靠右)、center(居中)
falignStringtruetable footer(表脚,的对齐方式,有:left(靠左)、right(靠右)、center(居中)
valignStringtrue每格数据的对齐方式,有:top(靠上)、middle(居中)、bottom(靠下)
widthNumberNull每列的宽度。如果没有自定义宽度自适应
widthUnitStringpx定义用于选项的单位,例如%
sortableBooleanfalse默认false就默认显示,设为true则会被排序
orderStringasc默认的排序方式为"asc(升序)",也可以设为"desc(降序)"。
visibleBooleantrue默认为true显示该列,设为false则隐藏该列
ignoreBooleanfalse默认为false该列可见,设为true则不可见,列选项也消失了
cardVisibleBooleantrue默认为true显示该列,设为false则隐藏。
switchableBooleantrue默认为true显示该列,设为false则禁用列项目的选项卡。
clickToSelectBooleantrue默认true不响应,设为false则当点击此行的某处时,不会自动选中此行的checkbox(复选框)或radiobox(单选按钮)
formatterFunctionNull某格的数据转换函数,需要三个参数: -value: field(字段名) -row:行的数据 -index:行的(索引)index
footerFormatterFunctionNull某格的数据转换函数,需要一个参数: -data: 所有行数据的数组 函数需要返回(return)footer某格内所要显示的字符串的格式
eventsObjectNull当某格使用formatter函数时,事件监听会响应,需要四个参数: -event:-value:字段名 -row:行数据 -index:此行的index
sorterFunctionNull自定义的排序函数,实现本地排序,需要两个参数: - a:第一个字段名 - b:第二个字段名
sortNameStringNull排序列名称
cellStyleFunctionNull对某列中显示样式改变
searchableBooleantrue默认true,表示此列数据可被查询
searchFormatterBooleantrue默认true,可使用格式化的数据查询
escapeBooleanfalse是否转义HTML字符串

使用方法

  • 表单搜索 $.table.search
<a onclick="$.table.search();">搜索</a>
  • 表格数据导出 $.table.exportExcel
<a onclick="$.table.exportExcel();">导出</a>
  • 数据模板下载 $.table.importTemplate
<a onclick="$.table.importTemplate();">下载模板</a>
  • 表格数据导入 $.table.importExcel
<a onclick="$.table.importExcel();">导入</a>
<form
  id="importForm"
  enctype="multipart/form-data"
  class="mt20 mb10"
  style="display: none;"
>
  <div class="col-xs-offset-1">
    <input type="file" id="file" name="file" />
    <div class="mt10 pt5">
      <input
        type="checkbox"
        id="updateSupport"
        name="updateSupport"
        title="如果登录账户已经存在,更新这条数据。"
      />
      是否更新已经存在的用户数据 &nbsp;
      <a onclick="$.table.importTemplate()" class="btn btn-default btn-xs"
        ><i class="fa fa-file-excel-o"></i> 下载模板</a
      >
    </div>
    <font color="red" class="pull-left mt10">
      提示:仅允许导入“xls”或“xlsx”格式文件!
    </font>
  </div>
</form>
  • 表格销毁 $.table.destroy
<a onclick="$.table.destroy();">销毁</a>
  • 表格数据刷新 $.table.refresh
<a onclick="$.table.refresh();">刷新</a>
  • 选择表格行具体列 $.table.selectColumns
var loginName = $.table.selectColumns("loginName");
  • 选择表格行首列 $.table.selectFirstColumns
var firstColumn = $.table.selectFirstColumns();
  • 显示表格特定的列 $.table.showColumn
$.table.showColumn("userName");
  • 隐藏表格特定的列 $.table.hideColumn
$.table.hideColumn("userName");
  • 序列号生成 $.table.serialNumber
{
	title: "序号",
	formatter: function (value, row, index) {
		return $.table.serialNumber(index);
	}
},
  • 超出指定长度浮动提示(单击文本可复制) $.table.tooltip
{
	field: 'remark',
	title: '备注',
	align: 'center',
	formatter: function(value, row, index) {
		return $.table.tooltip(value);
	}
},
  • 回显数据字典 $.table.selectDictLabel
var datas = [[${@dict.getType('sys_common_status')}]];
{
	field: 'status',
	title: '用户状态',
	align: 'center',
	formatter: function(value, row, index) {
		return $.table.selectDictLabel(datas, value);
	}
},
  • 新增回显数据字典(字符串数组) $.table.selectDictLabels
var datas = [[${@dict.getType('sys_common_status')}]];
{
	field: 'status',
	title: '用户状态',
	align: 'center',
	formatter: function(value, row, index) {
		return $.table.selectDictLabels(datas, value);
	}
},
  • 下拉按钮切换 $.table.dropdownToggle
formatter: function(value, row, index) {
	var actions = [];
	actions.push('<a class="' + editFlag + '" href="#" onclick="$.operate.edit(\'' + row.deptId + '\')"><i class="fa fa-edit"></i>编辑</a>');
	actions.push('<a class="' + removeFlag + '" href="#" onclick="$.operate.remove(\'' + row.deptId + '\')"><i class="fa fa-trash"></i>删除</a>');
	actions.push('<a class="' + addFlag + '" href="#" onclick="$.operate.add(\'' + row.deptId + '\')"><i class="fa fa-plus"></i>添加下级部门</a>');
	return $.table.dropdownToggle(actions.join(''));
}
  • 图片预览 $.table.imageView
{
	field: 'avatar',
	title: '用户头像',
	formatter: function(value, row, index) {
		return $.table.imageView(value);
	}
},

弹层使用

弹层组件目前基于layer组件进行封装,提供了弹出、消息、提示、确认、遮罩处理等功能。

  • 提供成功、警告和错误等反馈信息
$.modal.msg("默认反馈");
$.modal.msgError("错误反馈");
$.modal.msgSuccess("成功反馈");
$.modal.msgWarning("警告反馈");
  • 提供成功、警告和错误等提示信息
$.modal.alert("默认提示");
$.modal.alertError("错误提示");
$.modal.alertSuccess("成功提示");
$.modal.alertWarning("警告提示");
$.modal.confirm("确认信息", function () {});
  • 提供弹出层信息
// 弹出窗体 title 标题 url 请求链接 width 宽度 height 高度 callback 回调函数
$.modal.open(title, url, width, height, callback);

// 选卡页方式打开 title 标题 url 请求链接
$.modal.openTab(title, url);

// 选卡页方式打开并刷新当前页 isRefresh 是否刷新
$.modal.openTab(title, url, true);

// 选卡页同一页签打开
$.modal.parentTab(title, url);

// 弹出窗体 自定义 options 选项
$.modal.openOptions(options);

// 全屏弹出窗体
$.modal.openFull(title, url, width, height);

// 关闭窗体 index 当前层索引
$.modal.close(index);

// 关闭全部窗体
$.modal.closeAll();

// 关闭选项卡
$.modal.closeTab(dataId);

// 重新加载
$.modal.reload();
  • 提供遮罩层信息
// 打开遮罩层
$.modal.loading("正在导出数据,请稍后...");

// 关闭遮罩层
$.modal.closeLoading();

权限使用

使用thymeleaf模板整合了shiro标签,界面可以直接shiro:xxxx(此处简单介绍几个,更多请参考thymeleaf-extras-shiro (opens new window))

<!-- 角色权限 -->
<a href="#" shiro:hasRole="admin">管理员才能看到</a>
<a href="#" shiro:lacksRole="admin">验证用户是否不具备某角色</a>
<a href="#" shiro:hasAllRoles="admin,common">验证用户是否具有以下多个角色</a>
<a href="#" shiro:hasAnyRoles="admin,common"
  >验证用户是否具有以下任意一个角色</a
>
<!-- 资源权限 -->
<a href="#" shiro:hasPermission="system:user:add">包含权限字符串才能看到</a>
<a href="#" shiro:lacksPermission="system:user:add">验证用户是否不具备某权限</a>
<a href="#" shiro:hasAllPermissions="system:user:add,system:user:edit"
  >验证用户是否具有以下多个权限</a
>
<a href="#" shiro:hasAnyPermissions="system:user:add,system:user:edit"
  >验证用户是否具有以下任意一个权限</a
>

如果需要在JS中使用权限,使用封装方法

// 验证用户是否具备某权限
var permission = [[${@permission.hasPermi('system:user:add')}]];
// 验证用户是否不具备某权限
var permission = [[${@permission.lacksPermi('system:user:add')}]];
// 验证用户是否具有以下任意一个权限
var permission = [[${@permission.hasAnyPermi('system:user:add,system:user:edit')}]];
// 验证用户是否具备某角色
var role = [[${@permission.hasRole('admin')}]];
// 验证用户是否不具备某角色
var role = [[${@permission.lacksRole('admin')}]];
// 验证用用户是否具有以下任意一个角色
var role = [[${@permission.hasAnyRoles('admin,common')}]];
// 验证用户是否认证通过或已记住的用户
var isLogin = [[${@permission.isUser()}]];

// 追加标识可以实现隐藏
<a class="btn btn-success btn-xs ' + permission + '">包含权限字符串才能看到</a>
<a class="btn btn-danger btn-xs ' + role + '">管理员才能看到</a>

字典使用

配置好相关的数据字典信息即可正常使用(系统管理-字典管理)

<select name="status" th:with="type=${@dict.getType('sys_normal_disable')}">
  <option value="">所有</option>
  <option
    th:each="dict : ${type}"
    th:text="${dict.dictLabel}"
    th:value="${dict.dictValue}"
  ></option>
</select>
<label class="col-sm-2 control-label">回显数据字典值:</label>
<div
  class="form-control-static"
  th:text="${@dict.getLabel('sys_normal_disable', status)}"
></div>

如果在想Table表格数据使用字典,使用formatter格式化

// 获取数据字典数据
var datas = [[${@dict.getType('sys_normal_disable')}]];

// 格式化数据字典
formatter: function(value, row, index) {
	return $.table.selectDictLabel(datas, value);
}

参数使用

配置好相关的参数信息即可正常使用(系统管理-参数管理)

<body th:classappend="${@config.getKey('sys.index.skinName')}"></body>

如果需要在JS中使用参数,使用封装方法

var skinName = [[${@config.getKey('sys.index.skinName')}]];
$("#id").val(skinName);

图标使用

项目默认使用了Font Awesome和Glyphicons字体图标。

Font Awesome 默认图标示例代码 @/demo/icon/fontawesome.html(opens new window)Glyphicons 默认图标示例代码 @/demo/icon/glyphicons.html(opens new window)

两者都可以在项目中任意地方使用。

Font Awesome 使用方式

<!-- class 为 fontawesome 图标的名字 -->
<i class="fa fa-music"></i>

Glyphicons 使用方式

<!-- class 为 glyphicons 图标的名字 -->
<i class="glyphicon glyphicon-music"></i>

表单校验

Query Validate插件提供了强大的表单验证功能,能够让客户端表单验证变得更简单,同时它还提供了大量的可定制化选项,以满足应用程序的各种需求。该插件捆绑了一套非常有用的验证方法,包括 URL 和电子邮件验证,同时也提供了API允许用户自定义校验方法。

使用方式:$("#id").validate({});

// 全部校验
$("#form-xxx").validate().form();

// 清空校验
$("#form-xxx").validate().resetForm();

// 单个校验
$("#form-xxx").validate().element($("#xxx"));

默认校验规则

校验规则类型说明
requiredBoolean必输字段
remoteJsonString
emailBoolean必须输入正确格式的电子邮件
urlBoolean必须输入正确格式的网址
dateBoolean必须输入正确格式的日期
dateISOBoolean必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22 只验证格式,不验证有效性
numberBoolean必须输入合法的数字(负数,小数)
digitsBoolean必须输入整数
creditcardBoolean必须输入合法的信用卡号
equalTo:"#field"Selector输入值必须和#field相同
acceptString输入拥有合法后缀名的字符串(上传文件的后缀)
maxlength:5Number输入长度最多是5的字符串(汉字算一个字符)
minlength:10Number输入长度最小是10的字符串(汉字算一个字符)
rangelength:[5,10]Array输入长度必须介于 5 和 10 之间的字符串")(汉字算一个字符)
range:[5,10]Array输入值必须介于 5 和 10 之间
step:5Number输入值 5 的整数倍值
max:5Number输入值不能大于5
min:10Number输入值不能小于10
isPhoneBoolean必须输入正确格式的手机号
isTelBoolean必须输入正确格式的座机号码
isNameBoolean姓名只能用汉字
isUserNameBoolean必须输入数字或者字母,不包含特殊字符
isIdentityBoolean必须输入正确格式的身份证号码
isBirthBoolean必须输入正确格式的出生日期
isIpBoolean必须输入正确格式的IP地址
notEqual:xxxxString不允许为输入值
gt:10Number必须大于输入值

自定义校验规则

本文介绍Validate自定义表单校验方式。Validate插件虽然提供了丰富的验证规则,但在很多时候仍然很难满足我们的开发需求,在注册页面我们需要通过ajax验证用户输入的用户名是否已经被他人注册,那此时通过传统的Validate验证方式已经无法满足需求了! 我们可以通过自定义验证方法实现这个需求。

例如加一个区号的验证。

1、在jquery.validate.extend.js加入自定义规则

// 地区号码验证
jQuery.validator.addMethod(
  "ac",
  function (value, element) {
    var ac = /^0\d{2,3}$/;
    return this.optional(element) || ac.test(value);
  },
  "区号如:010或0371",
);

2、然后rules中使用ac: true

rules: {
    areaCode:{
        ac: true,
    },
}

出现如下图表示自定义区号验证成功。 validate

使用tooltip提示错误信息

unhighlight表示element验证正确时触发,errorPlacement表示element验证错误时触发。

unhighlight: function(element, errorClass, validClass) {
	$(element).tooltip('destroy').removeClass(errorClass);
},
errorPlacement: function(error, element) {
	if ($(element).next("div").hasClass("tooltip")) {
		$(element).attr("data-original-title", $(error).text()).tooltip("show");
	} else {
		$(element).attr("title", $(error).text()).tooltip("show");
	}
},

功能组件

弹窗功能

弹框的功能不管是在传统开发中还是如今比较流行的前后端分离开发中都是比较常见的功能,如:添加、编辑、确认框提示等等(当前页可以直接打开新页面), 为了解决这个问题,我们封装了弹框组件,根据使用场景的不同,框架做了继承开发,调用时只需要传入相应的参数即可。

函数主体:

open: function (title, url, width, height, callback) {
	//如果是移动端,就使用自适应大小弹窗
	if ($.common.isMobile()) {
		width = 'auto';
		height = 'auto';
	}
	if ($.common.isEmpty(title)) {
		title = false;
	}
	if ($.common.isEmpty(url)) {
		url = "/404.html";
	}
	if ($.common.isEmpty(width)) {
		width = 800;
	}
	if ($.common.isEmpty(height)) {
		height = ($(window).height() - 50);
	}
	if ($.common.isEmpty(callback)) {
		callback = function(index, layero) {
			var iframeWin = layero.find('iframe')[0];
			iframeWin.contentWindow.submitHandler(index, layero);
		}
	}
	layer.open({
		type: 2,
		area: [width + 'px', height + 'px'],
		fix: false,
		//不固定
		maxmin: true,
		shade: 0.3,
		title: title,
		content: url,
		btn: ['确定', '关闭'],
		// 弹层外区域关闭
		shadeClose: true,
		yes: callback,
		cancel: function(index) {
			return true;
		}
	});
},

参数说明:

  • title 弹窗标题,这个标题是在弹框的左上角显示的标题文字
  • url URL地址,这个是弹框调用的方法地址,比如添加、编辑时需要调用页面表单地址的方法
  • width 弹窗宽度,一个数值(不传时默认弹窗自适应显示)
  • height 弹窗高度,一个数值(不传时默认弹窗自适应显示)
  • callback 回调函数,弹窗成功弹出之后会默认进行回调

调用方式:

// 普通调用
$.modal.open("标题内容", url);

// 设置宽高
$.modal.open("标题内容", url, "770", "380");

// 设置回调函数
$.modal.open("标题内容", url, "770", "380", function (index, layero) {
  // 获取弹窗参数(方式一)
  var body = layer.getChildFrame("body", index);
  console.log(body.find("#id").val());
  // 获取弹窗参数(方式二)
  console.log(
    $(layero).find("iframe")[0].contentWindow.document.getElementById("id")
      .value,
  );
});

新增功能

新增方法我们写一个共用的方法add,下面我们详细的描述下,新增时是如何弹出窗体的。

函数主体:

// 添加信息
add: function(id) {
	table.set();
	$.modal.open("添加" + table.options.modalName, $.operate.addUrl(id));
},

// 添加访问地址
addUrl: function(id) {
	var url = $.common.isEmpty(id) ? table.options.createUrl.replace("{id}", "") : table.options.createUrl.replace("{id}", id);
	return url;
},

后端代码:

// 添加方法(默认)
@GetMapping("/add")
public String add()
{
	return prefix + "/add";
}

// 添加方法(带id参数)
@GetMapping("/add/{xxId}")
public String add(@PathVariable("xxId") Long xxId, ModelMap mmap)
{
	mmap.put("xxxx", xxxxService.selectXxxxById(xxId));
	return prefix + "/add";
}

参数说明:

  • id 需要传入到后台的唯一标识

总结:add方法里面进行了判断存在ID则进行内容替换,然后进行调用弹窗操作。 操作table.options.createUrl地址,弹窗table.options.modalName标题

调用方式:

// 普通调用
$.operate.add();

// 传参调用,例如:/system/user/add/{1} 会被替换为 /system/user/add/1
$.operate.add(1);

修改功能

修改方法我们写一个共用的方法edit,下面我们详细的描述下,修改时是如何弹出窗体的。

函数主体:

/ 修改信息
edit: function(id) {
	table.set();
	if($.common.isEmpty(id) && table.options.type == table_type.bootstrapTreeTable) {
		var row = $("#" + table.options.id).bootstrapTreeTable('getSelections')[0];
		if ($.common.isEmpty(row)) {
			$.modal.alertWarning("请至少选择一条记录");
			return;
		}
		var url = table.options.updateUrl.replace("{id}", row[table.options.uniqueId]);
		$.modal.open("修改" + table.options.modalName, url);
	} else {
		$.modal.open("修改" + table.options.modalName, $.operate.editUrl(id));
	}
},

// 修改访问地址
editUrl: function(id) {
	var url = "/404.html";
	if ($.common.isNotEmpty(id)) {
		url = table.options.updateUrl.replace("{id}", id);
	} else {
		var id = $.common.isEmpty(table.options.uniqueId) ? $.table.selectFirstColumns() : $.table.selectColumns(table.options.uniqueId);
		if (id.length == 0) {
			$.modal.alertWarning("请至少选择一条记录");
			return;
		}
		url = table.options.updateUrl.replace("{id}", id);
	}
	return url;
},

后端代码:

// 修改方法
@GetMapping("/edit/{xxId}")
public String edit(@PathVariable("xxId") Long xxId, ModelMap mmap)
{
	mmap.put("xxxx", xxxxService.selectXxxxById(xxId));
	return prefix + "/edit";
}

参数说明:

  • id 需要传入到后台的唯一标识

总结:edit方法里面进行了判断存在ID则进行内容替换,然后进行调用弹窗操作。 优先级:传参ID值 -> 选择uniqueId列值 -> 选择首列值 操作table.options.updateUrl地址,table.options.uniqueId唯一的标识符,弹窗table.options.modalName标题

调用方式:

// 普通调用
$.operate.edit();

// 传参调用,例如:/system/user/edit/{1} 会被替换为 /system/user/edit/1
$.operate.edit(1);

删除功能

删除功能我们并不陌生,在大多数的数据列表中,都会有删除按钮的出现,顾名思义就是在有权限的前提下我们可以删除这条数据,点击删除会弹出删除确认框, 确定删除后才会发起网络请求,具体JS实现方法如下:

函数主体:

// 删除信息
remove: function(id) {
	table.set();
	$.modal.confirm("确定删除该条" + table.options.modalName + "信息吗?", function() {
		var url = $.common.isEmpty(id) ? table.options.removeUrl : table.options.removeUrl.replace("{id}", id);
		if(table.options.type == table_type.bootstrapTreeTable) {
			$.operate.get(url);
		} else {
			var data = { "ids": id };
			$.operate.submit(url, "post", "json", data);
		}
	});
},

后端代码:

// 删除方法
@GetMapping("/remove/{xxId}")
@ResponseBody
public AjaxResult remove(@PathVariable("xxId") Long xxId)
{
	return toAjax(xxxxService.deleteXxxxById(xxId));
}

参数说明:

  • id 需要传入到后台的唯一标识

总结:remove方法里面进行了判断存在ID则进行内容替换,同时弹出确认提醒。 操作table.options.removeUrl地址,弹窗table.options.modalName标题

调用方式:

// 传参调用,例如:/system/user/remove/{1} 会被替换为 /system/user/remove/1
$.operate.remove(1);

批量删除

批量操作就是可以选择多条记录数据进行批量处理的方法,在我们使用中批量删除算是最常用的了,删除多条数据时一条条删除非常耗时,那么批量删除可以很好的帮我解决此问题。

函数主体:

// 批量删除信息
removeAll: function() {
	table.set();
	var rows = $.common.isEmpty(table.options.uniqueId) ? $.table.selectFirstColumns() : $.table.selectColumns(table.options.uniqueId);
	if (rows.length == 0) {
		$.modal.alertWarning("请至少选择一条记录");
		return;
	}
	$.modal.confirm("确认要删除选中的" + rows.length + "条数据吗?", function() {
		var url = table.options.removeUrl;
		var data = { "ids": rows.join() };
		$.operate.submit(url, "post", "json", data);
	});
},

后端代码:

// 批量删除方法
@PostMapping("/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
    return toAjax(xxxxService.deleteXxxxByIds(ids));
}

总结:removeAll方法里面默认会找到选择的第一列,其他情况可以设置指定列uniqueId即可。 操作table.options.removeUrl地址,弹窗table.options.modalName标题

调用方式:

$.operate.removeAll();

查看详情

查看记录数据的详情在我们项目研发中也是非常多见的,因此基于使用频率,我们也内置集成了查看详情的功能。

函数主体:

// 详细信息
detail: function(id, width, height) {
	table.set();
	var _url = $.operate.detailUrl(id);
	var _width = $.common.isEmpty(width) ? "800" : width;
	var _height = $.common.isEmpty(height) ? ($(window).height() - 50) : height;
	//如果是移动端,就使用自适应大小弹窗
	if ($.common.isMobile()) {
		_width = 'auto';
		_height = 'auto';
	}
	var options = {
		title: table.options.modalName + "详细",
		width: _width,
		height: _height,
		url: _url,
		skin: 'layui-layer-gray',
		btn: ['关闭'],
		yes: function (index, layero) {
			layer.close(index);
		}
	};
	$.modal.openOptions(options);
},

// 详细访问地址
detailUrl: function(id) {
	var url = "/404.html";
	if ($.common.isNotEmpty(id)) {
		url = table.options.detailUrl.replace("{id}", id);
	} else {
		var id = $.common.isEmpty(table.options.uniqueId) ? $.table.selectFirstColumns() : $.table.selectColumns(table.options.uniqueId);
		if (id.length == 0) {
			$.modal.alertWarning("请至少选择一条记录");
			return;
		}
		url = table.options.detailUrl.replace("{id}", id);
	}
	return url;
},

后端代码:

// 查询详细方法
@GetMapping("/detail/{xxId}")
public String detail(@PathVariable("xxId") Long xxId, ModelMap mmap)
{
	mmap.put("xxxx", xxxxService.selectXxxxById(xxId));
	return prefix + "/detail";
}

参数说明:

  • id 需要传入到后台的唯一标识
  • width 弹窗宽度,一个数值(不传时默认弹窗自适应显示)
  • height 弹窗高度,一个数值(不传时默认弹窗自适应显示)

总结:detail方法里面进行了判断存在ID则进行内容替换,同时可以传入指定宽度高度。 操作table.options.detailUrl地址,弹窗table.options.modalName标题

调用方式:

// 传参调用,例如:/system/user/detail/{1} 会被替换为 /system/user/detail/1
$.operate.detail(1);

// 设置宽高
$.operate.detail(1, "770", "380");

搜索功能

对于大量的数据列表而言,我们常常需要根据条件获得我们所需要的数据源,这是条件搜索就可以帮助我们实现,正如很多模块我们所看到的那样在数据列表上方有很多的条件筛选框, 这是我们可以选择我们所需要查询的条件,然后去定向搜索,鉴于此框架也做了常规的集成。

函数主体:

// 搜索-默认第一个form
search: function(formId, tableId) {
	table.set(tableId);
	table.options.formId = $.common.isEmpty(formId) ? $('form').attr('id') : formId;
	var params = $.common.isEmpty(tableId) ? $("#" + table.options.id).bootstrapTable('getOptions') : $("#" + tableId).bootstrapTable('getOptions');
	if($.common.isNotEmpty(tableId)){
		$("#" + tableId).bootstrapTable('refresh', params);
	} else{
		$("#" + table.options.id).bootstrapTable('refresh', params);
	}
},

后端代码:

// 查询方法
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(Xxxx xxxx)
{
	startPage();
	List<Xxxx> list = xxxxService.selectXxxxList(xxxx);
	return getDataTable(list);
}

参数说明:

  • formId 查询表单ID
  • tableId 查询表格ID

总结:默认查询第一个表单form以及表格table.options.id,search方法里面也进行了判断formId,tableId可以传入指定表单ID,表格ID查询。

调用方式:

// 普通查询调用
$.table.search();

// 查询指定表单ID
$.table.search("formId");

// 查询查询表单ID,表格ID
$.table.search("formId", "tableId");

导入功能

对于数据量大,导入可以大大提高使用者的使用效率,鉴于此框架也做了常规的集成。

函数主体:

// 导入数据
importExcel: function(formId, width, height) {
	table.set();
	var currentId = $.common.isEmpty(formId) ? 'importTpl' : formId;
	var _width = $.common.isEmpty(width) ? "400" : width;
	var _height = $.common.isEmpty(height) ? "230" : height;
	layer.open({
		type: 1,
		area: [_width + 'px', _height + 'px'],
		fix: false,
		//不固定
		maxmin: true,
		shade: 0.3,
		title: '导入' + table.options.modalName + '数据',
		content: $('#' + currentId).html(),
		btn: ['<i class="fa fa-check"></i> 导入', '<i class="fa fa-remove"></i> 取消'],
		// 弹层外区域关闭
		shadeClose: true,
		btn1: function(index, layero){
			var file = layero.find('#file').val();
			if (file == '' || (!$.common.endWith(file, '.xls') && !$.common.endWith(file, '.xlsx'))){
				$.modal.msgWarning("请选择后缀为 “xls”或“xlsx”的文件。");
				return false;
			}
			var index = layer.load(2, {shade: false});
			$.modal.disable();
			var formData = new FormData(layero.find('form')[0]);
			$.ajax({
				url: table.options.importUrl,
				data: formData,
				cache: false,
				contentType: false,
				processData: false,
				type: 'POST',
				success: function (result) {
					if (result.code == web_status.SUCCESS) {
						$.modal.closeAll();
						$.modal.alertSuccess(result.msg);
						$.table.refresh();
					} else if (result.code == web_status.WARNING) {
						layer.close(index);
						$.modal.enable();
						$.modal.alertWarning(result.msg)
					} else {
						layer.close(index);
						$.modal.enable();
						$.modal.alertError(result.msg);
					}
				}
			});
		}
	});
},

后端代码:

// 导入方法
@PostMapping("/importData")
@ResponseBody
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
	ExcelUtil<Xxxx> util = new ExcelUtil<Xxxx>(Xxxx.class);
	List<Xxxx> xxxxList = util.importExcel(file.getInputStream());
	String message = xxxxService.importXxxx(xxxxList, updateSupport);
	return AjaxResult.success(message);
}

参数说明:

  • formId 显示指定表单ID元素内容
  • width 弹窗宽度,一个数值(不传时默认弹窗400显示)
  • height 弹窗高度,一个数值(不传时默认弹窗230显示)

总结:importExcel方法里面进行了判断不存在formId参数则使用默认的importTpl,同时在回调函数实现了文件上传的ajax请求。 操作table.options.importUrl地址,弹窗table.options.modalName标题

调用方式:

// 普通查询调用
$.table.importExcel();

// 显示指定表单ID
$.table.importExcel("formId");

// 显示指定表单ID,同时设置宽高
$.table.importExcel("formId", "770", "380");

下载模版

对于一些情况在导入前需要下载自定义模块的情况,也做了常规的集成。

函数主体:

// 下载模板
importTemplate: function() {
	table.set();
	$.get(table.options.importTemplateUrl, function(result) {
		if (result.code == web_status.SUCCESS) {
			window.location.href = ctx + "common/download?fileName=" + encodeURI(result.msg) + "&delete=" + true;
		} else if (result.code == web_status.WARNING) {
			$.modal.alertWarning(result.msg)
		} else {
			$.modal.alertError(result.msg);
		}
	});
},

后端代码:

// 下载模板
@GetMapping("/importTemplate")
@ResponseBody
public AjaxResult importTemplate()
{
	ExcelUtil<Xxxx> util = new ExcelUtil<Xxxx>(Xxxx.class);
	return util.importTemplateExcel("xx数据");
}

总结:importTemplate会直接请求后端接口,生成模块成功后进行下载操作。 操作table.options.importTemplateUrl地址。

调用方式:

$.table.importTemplate();

导出功能

项目中经常需要使用导入导出功能来加快数据的操作,框架实现的方式是先读取数据后上传到本地,等文件生成后,将文件传送到存储在本地磁盘(也可以改成CDN)进行存储,然后返回上传后的存储地址。 前端需要调用下载请求后再进行处理。对于高耗时的下载请求时,会有一定的优化。

函数主体:

// 导出数据
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 });
		$.modal.loading("正在导出数据,请稍后...");
		$.post(table.options.exportUrl, dataParam, function(result) {
			if (result.code == web_status.SUCCESS) {
				window.location.href = ctx + "common/download?fileName=" + encodeURI(result.msg) + "&delete=" + true;
			} else if (result.code == web_status.WARNING) {
				$.modal.alertWarning(result.msg)
			} else {
				$.modal.alertError(result.msg);
			}
			$.modal.closeLoading();
		});
	});
},

后端代码:

// 导出数据
@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, "xx数据");
}

参数说明:

  • formId 显示指定表单ID元素内容

总结:exportExcel默认导出参数为第一个表单form,同时也可以传入指定formId。 操作table.options.exportUrl地址,弹窗table.options.modalName标题

调用方式:

// 普通查询调用
$.table.exportExcel();

// 导出指定表单参数
$.table.exportExcel("formId");

提交功能

提交功能我们并不陌生,在新增和修改页面中,都会有提交按钮的出现,顾名思义就是提交这条数据到后台处理。

函数主体:

// 提交数据
save: function(url, data, callback) {
	var config = {
		url: url,
		type: "post",
		dataType: "json",
		data: data,
		beforeSend: function () {
			$.modal.loading("正在处理中,请稍后...");
			$.modal.disable();
		},
		success: function(result) {
			if (typeof callback == "function") {
				callback(result);
			}
			$.operate.successCallback(result);
		}
	};
	$.ajax(config)
},

后端代码:

// 新增提交
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@Validated Xxxx xxxx)
{
	return toAjax(xxxxService.insertXxxx(xxxx));
}

// 修改提交
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(@Validated Xxxx xxxx)
{
	return toAjax(xxxxService.updateXxxx(xxxx));
}

参数说明:

  • url 提交的后台地址
  • data 提交到后台的数据
  • callback 回调函数,提交成功之后会默认进行回调

调用方式:

// 普通调用
$.operate.save(prefix + "/add", $("#form-xxxx").serialize());

// 提交设置回调函数
$.operate.save(prefix + "/add", $("#form-xxxx").serialize(), function (result) {
  // 状态码
  console.log(result.code);
  // 消息内容
  console.log(result.msg);
});

模版引擎

Thymeleaf

Spring Boot中推荐使用Thymeleaf作为模板引擎,因为Thymeleaf提供了完美的Spring MVC支持,它是一个跟Velocity、FreeMarker类似的模板引擎,它可以完全替代JSP。相较与其他的模板引擎,它有如下三个极吸引人的特点

  • Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持html原型,然后在html标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释html时会忽略未定义的标签属性,所以thymeleaf的模板可以静态地运行;当有数据返回到页面时,Thymeleaf标签会动态地替换掉静态内容,使页面动态显示。
  • Thymeleaf 开箱即用的特性。它提供标准和Spring标准两种方言,可以直接套用模板实现JSTL、OGNL表达式效果,避免每天套模板、改JSTL、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
  • Thymeleaf 提供Spring标准方言和一个与SpringMVC完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

对象使用

文本国际化获取

<div th:with="num=5">
  <p>1、普通获取方式:[[#{user.password.not.match}]]</p>
  <p>2、传入索引方式:[[#{user.password.retry.limit.exceed(${num})}]]</p>
</div>

获取变量方式

<div>
  <p>1、获取对象中的userName属性:[[${user.userName}]]</p>
  <p>2、获取集合中的元素:[[${users[0].userName}]]</p>
  <p>3、获取Map中的元素:[[${userMap['user2'].userName}]]</p>
  <p>4、获取数组中的元素:[[${userArr[0].userName}]]</p>
</div>

处理转义文本

<div th:with="html='<h3>若依框架</h3>'">
  <p>1、转义字符输出</p>
  <p th:text="${html}"></p>
  <p>2、无需字符转义</p>
  <p th:utext="${html}"></p>
</div>

链接表达式

<div>
  <a th:href="@{/user/details(userId=${user.userId})}">设置单个URL参数</a>
  <a th:href="@{/user/details(userId=${user.userId},name=${user.userName})}"
    >设置多个URL参数</a
  >
  <a th:href="@{/user/{userId}/details(userId=${user.userId})}"
    >设置rest风格参数</a
  >
</div>

数字对象

Thymeleaf主要使用org.thymeleaf.expression.Numbers这个类来处理数字,在模板中使用#numbers来表示这个对象。

<div th:with="num=1000">
  1、整数格式化
  <p>[[${#numbers.formatInteger(num,5)}]]</p>
  <p th:each="arrNum : ${#numbers.arrayFormatInteger(arr,5)}">[[${arrNum}]]</p>
  <p>[[${#numbers.listFormatInteger(list,5)}]]</p>
  <p>[[${#numbers.setFormatInteger(set,5)}]]</p>

  2、小数格式化
  <p>[[${#numbers.formatDecimal(10.992, 3, 2)}]]</p>
  <p th:each="arrNum : ${#numbers.arrayFormatDecimal(arr, 3, 2)}">
    [[${arrNum}]]
  </p>
  <p>[[${#numbers.listFormatDecimal(list, 3, 2)}]]</p>
  <p>[[${#numbers.setFormatDecimal(set, 3, 2)}]]</p>

  3、标识千位分隔符
  <p>POINT使用"."作为分隔符:[[${#numbers.formatInteger(num,5,'POINT')}]]</p>
  <p>COMMA使用","作为分隔符:[[${#numbers.formatInteger(num,5,'COMMA')}]]</p>
  <p>
    WHITESPACE使用"
    "作为分隔符:[[${#numbers.formatInteger(num,5,'WHITESPACE')}]]
  </p>

  4、货币格式化
  <p>[[${#numbers.formatCurrency(10.99)}]]</p>
  <p>
    arrayFormatCurrency、listFormatCurrency、setFormatCurrency和上面一样,就不演示了
  </p>

  5、百分比格式化
  <p>[[${#numbers.formatPercent(0.23456, 2, 3)}]]</p>
</div>

字符串对象

Thymeleaf主要使用org.thymeleaf.expression.Strings这个类来处理数字,在模板中使用#strings来表示这个对象。

<div th:with="html='<h3>若依框架</h3>'">
  1、toString与length方法
  <p>toString方法:[[${#strings.toString(user)}]]</p>
  <p>length方法:[[${#strings.length(user)}]]</p>

  2、非空判断与默认值处理
  <p>isEmpty方法:[[${#strings.isEmpty(user.userName)}]]</p>
  <p>defaultString方法:[[${#strings.defaultString(user.sex, '无性别')}]]</p>

  3、包含判断
  <p>contains方法:[[${#strings.contains(user.userName, '张三')}]]</p>
  <p>
    containsIgnoreCase方法:[[${#strings.containsIgnoreCase(user.userName,
    '张三')}]]
  </p>
  <p>startsWith方法:[[${#strings.startsWith(user.userName, '张')}]]</p>
  <p>endsWith方法:[[${#strings.endsWith(user.userName, '三')}]]</p>

  4、截取与替换
  <p>indexOf方法:[[${#strings.indexOf('abcde', 'z')}]]</p>
  <p>substring方法:[[${#strings.substring('abcde', 1, 3)}]]</p>
  <p>substringAfter方法:[[${#strings.substringAfter('abcde', 'a')}]]</p>
  <p>substringBefore方法:[[${#strings.substringBefore('abcde', 'c')}]]</p>
  <p>replace方法:[[${#strings.replace('abcde', 'c', '1')}]]</p>

  5、追加与拼接
  <p>prepend方法:[[${#strings.prepend(3, 'e')}]]</p>
  <p>append方法:[[${#strings.append('abcd', 'e')}]]</p>
  <p>concat方法:[[${#strings.concat('abcd', 'e', 'b')}]]</p>
  <p>
    concatReplaceNulls方法:[[${#strings.concatReplaceNulls('**', '123', null,
    'abc')}]]
  </p>

  6、分割与连接
  <p class="text-left" th:each="array : ${#strings.arraySplit('a-b-c', '-')}">
    arraySplit方法:[[${array}]]
  </p>
  <p>
    listSplit方法:[[${#strings.arrayJoin(new String[]{'a','b','c'}, '-')}]]
  </p>
  <p>listSplit、setSplit、listJoin、setJoin和上面一样,就不演示了</p>

  7、大小写转换
  <p>toUpperCase:[[${#strings.toUpperCase('spring boot')}]]</p>
  <p>toLowerCase:[[${#strings.toLowerCase('Spring Boot')}]]</p>
  <p>capitalize:[[${#strings.capitalize('spring boot')}]]</p>
  <p>unCapitalize:[[${#strings.unCapitalize('Spring boot')}]]</p>
  <p>capitalizeWords:[[${#strings.capitalizeWords('spring boot')}]]</p>
  <p>capitalizeWords:[[${#strings.capitalizeWords('spring-boot', '-')}]]</p>

  8、其他处理
  <p>trim:[[${#strings.trim(' spring boot ')}]]</p>
  <p>abbreviate:[[${#strings.abbreviate('SpringBooot', 9)}]]</p>
  <p>randomAlphanumeric:[[${#strings.randomAlphanumeric(5)}]]</p>
</div>

日期对象

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

<div>
  1、格式化日期
  <p>[[${#dates.format(date)}]]</p>
  <p>[[${#dates.formatISO(date)}]]</p>
  <p>[[${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}]]</p>

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

数组与集合对象

更多方法可以去org.thymeleaf.expression.xxxx查询相关源码。

<div>
  Thymeleaf主要使用
  <p>org.thymeleaf.expression.Arrays</p>
  <p>org.thymeleaf.expression.Lists</p>
  <p>org.thymeleaf.expression.Maps</p>
  <p>org.thymeleaf.expression.Sets</p>
  处理数组与集合对象
</div>

常用语法

运算符

表达式常量

<p><strong>字符串常量</strong> - <span th:text="'hello , word'"></span></p>
<p><strong>数字常量</strong> - <span th:text="2019 + 2"></span></p>
<p><strong>布尔值常量</strong> - <span th:text="${result}"></span></p>
<p><strong>空值常量</strong> - <span th:text="${result == null}"></span></p>

字符串拼接

<p>
  <strong>方法一</strong> -
  <span th:text="${#strings.append('abc', 'd')}"></span>
</p>
<p>
  <strong>方法二</strong> -
  <span th:text="${#strings.concat('abc', 'd')}"></span>
</p>
<p><strong>方法三</strong> - <span th:text="${'abc' + 'd'}"></span></p>
<p><strong>方法四</strong> - <span th:text="'abc' + 'd'"></span></p>
<p><strong>方法五</strong> - <span th:text="abc + d"></span></p>
<p><strong>方法六</strong> - <span th:text="|abc| + |d|"></span></p>

算数运算符

<p><strong>3 + 2 = </strong> <span th:text="${3 + 2}"></span></p>
<p><strong>5 - 2 = </strong> <span th:text="${5 - 2}"></span></p>
<p><strong>5 * 2 =</strong> <span th:text="${5* 2}"></span></p>
<p><strong>5 / 2 =</strong> <span th:text="5.0 / 2"></span></p>
<p><strong>5 % 2 =</strong> <span th:text="5 % 2"></span></p>

关系运算符

<p><strong>1 > 1 = </strong> <span th:text="${1 gt 1}"></span></p>
<p><strong>1 < 1 = </strong> <span th:text="${1 lt 1}"></span></p>
<p><strong>1 >= 1 = </strong> <span th:text="${1 ge 1}"></span></p>
<p><strong>1 <= 1 = </strong> <span th:text="${1 le 1}"></span></p>
<p><strong>1 == 1 = </strong> <span th:text="${1 eq 1}"></span></p>
<p><strong>1 != 1 = </strong> <span th:text="${1 ne 1}"></span></p>

三目运算符

<p>
  <strong>方法一</strong>
  <span th:text="${1 != 1} ? |条件成立| : |条件不成立|"></span>
</p>
<p>
  <strong>方法二</strong>
  <span th:text="${value == 'ruoyi'} ? ${value} : |默认值|"></span>
</p>
<p>
  <strong>方法三(不成立返回空)</strong>
  <span th:text="${1 != 1} ? |条件成立|"></span>
</p>

属性设置

<p>1、单个属性值设置:<input type="submit" value="提交" th:attr="value=#{user.login.success}"></input></p>
<p>2、多个属性值设置:<input type="submit" value="提交" th:attr="value=#{user.login.success},class='btn btn-primary'"></input></p>
<p>3、设置单个HTML节点属性:<input type="submit" value="提交" th:value="#{user.login.success}"></input></p>
<p>4、设置多个HTML节点属性:<input type="submit" value="提交" th:value="#{user.login.success}" th:class="'btn btn-primary'"></input></p>
<p>5、属性值后面拼接:<div id="imgDiv" th:attrappend="id='-dataId'"></div></p>
<p>6、属性值前面拼接:<div id="imgDiv" th:attrprepend="id='dataId-'"></div></p>
<p>7、属性添加style样式:<div style="text-align: left;" th:styleappend="'color:red'">style样式</div></p>
<p>8、属性添加class样式:<div class="btn btn-primary btn-xs" th:classappend="'btn-rounded'">class样式</div></p>

条件判断

<ul th:with="value=ruoyi">
  <li>
    <p>
      <strong>方法一</strong> - <span th:if="0">数字类型,如果为0,不显示</span>
    </p>
    <p>
      <strong>方法二</strong> - <span th:if="false">false、off、no 不显示</span>
    </p>
    <select th:with="sex=1" th:switch="${sex}">
      <option th:case="0">男</option>
      <option th:case="1">女</option>
    </select>
  </li>
</ul>

数据迭代

list数据迭代

<table>
  <tr th:each="user : ${users}">
    <td th:text="${user.userName}"></td>
  </tr>
</table>

map数据迭代

<table>
  <tr th:each="map : ${userMap}">
    <td th:text="${map.key}"></td>
    <td th:text="${map.value.userName}"></td>
  </tr>
</table>

数据状态对象(自定义状态对象)

<table class="table">
  <thead>
    <tr>
      <th>用户名</th>
      <th>从0开始的索引</th>
      <th>从1开始的索引</th>
      <th>数据集合大小</th>
      <th>是否第一次迭代</th>
      <th>是否最后一次迭代</th>
      <th>是否偶数次迭代</th>
      <th>是否奇数次迭代</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="user,state : ${users}">
      <td th:text="${user.userName}"></td>
      <td th:text="${state.index}"></td>
      <td th:text="${state.count}"></td>
      <td th:text="${state.size}"></td>
      <td th:text="${state.first}"></td>
      <td th:text="${state.last}"></td>
      <td th:text="${state.even}"></td>
      <td th:text="${state.odd}"></td>
    </tr>
  </tbody>
</table>

数据状态对象(默认规则 节点变量名+Stat)

<table class="table">
  <thead>
    <tr>
      <th>用户名</th>
      <th>从0开始的索引</th>
      <th>从1开始的索引</th>
      <th>数据集合大小</th>
      <th>是否第一次迭代</th>
      <th>是否最后一次迭代</th>
      <th>是否偶数次迭代</th>
      <th>是否奇数次迭代</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="user : ${users}">
      <td th:text="${user.userName}"></td>
      <td th:text="${userStat.index}"></td>
      <td th:text="${userStat.count}"></td>
      <td th:text="${userStat.size}"></td>
      <td th:text="${userStat.first}"></td>
      <td th:text="${userStat.last}"></td>
      <td th:text="${userStat.even}"></td>
      <td th:text="${userStat.odd}"></td>
    </tr>
  </tbody>
</table>

星号表达式

<table class="table">
  <thead>
    <tr>
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <tr th:each="user : ${users}" th:object="${user}">
      <td th:text="*{userId}"></td>
      <td th:text="*{userName}"></td>
    </tr>
  </tbody>
</table>

模板片段

定义与引用模板片段

<div th:with="year=2019">
  <div th:with="result=true">
    <p class="text-danger">定义与引用模板片段(~{模板名称::选择器})</p>
    th:insert : 保留自己的主标签,保留th:fragment的主标签。
    <div th:insert="~{fragment/footer.html :: copy}"></div>
    th:replace :不要自己的主标签,保留th:fragment的主标签。
    <div th:replace="~{fragment/footer.html :: copy}"></div>
    th:include :保留自己的主标签,不要th:fragment的主标签。
    <div th:include="~{fragment/footer.html :: copy}"></div>
  </div>
</div>

选择器的基础语法

<div>
	1、选择直接子节点id为footerA的div
	<p><div th:insert="~{fragment/select.html :: /div[@id='footerA']}"></div></p>
	2、选择全部子节点中id为footerB的div
	<p><div th:insert="~{fragment/select.html :: //div[@id='footerB']}"></div></p>
	3、选择class为content的span节点
	<p><div th:insert="~{fragment/select.html :: span[@class='content']}"></div></p>
	4、选择class为footerG的span(有多个),选出第一个
	<p><div th:insert="~{fragment/select.html :: //span[@class='footerG'][0]}"></div></p>
	5、选择class为footerContent并且id为footerE的span(多级筛选)
	<p><div th:insert="~{fragment/select.html :: //div[@class='footerContent']//span[@id='footerE']}"></div></p>
</div>

含有变量的片段引用

<div th:with="userName='张三',deptName='技术部'">
  1、使用常量传参
  <div th:replace="~{fragment/param.html :: welcome('张三','技术部')}"></div>
  2、使用变量传参
  <div
    th:replace="~{fragment/param.html :: welcome(${userName},${deptName})}"
  ></div>
  3、不传入参数情况(不会出现异常)
  <div th:replace="~{fragment/param.html :: welcome_1}"></div>
  4、不显示指定片段参数
  <div
    th:replace="~{fragment/param.html :: welcome_1(val1='张三', val2='技术部')}"
  ></div>
  5、片断块引用
  <table class="table">
    <thead>
      <tr>
        <th>用户ID</th>
        <th>用户名</th>
      </tr>
    </thead>
    <tbody>
      <th:block th:each="user : ${users}">
        <tr>
          <td th:text="${user.userId}"></td>
          <td th:text="${user.userName}"></td>
        </tr>
      </th:block>
    </tbody>
  </table>
</div>

删除模板

1、普通方法

<p><div th:if="false">我是当前节点<div>我是子节点</div></div></p>

2、remove删除方法(all删除包含标签和所有的子节点)

<table class="table">
  <thead>
    <tr th:remove="all">
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <th:block th:each="user : ${users}">
      <tr>
        <td th:text="${user.userId}"></td>
        <td th:text="${user.userName}"></td>
      </tr>
    </th:block>
  </tbody>
</table>

3、remove删除方法(body不包含标记删除,但删除其所有的子节点)

<table class="table">
  <thead>
    <tr th:remove="body">
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <th:block th:each="user : ${users}">
      <tr>
        <td th:text="${user.userId}"></td>
        <td th:text="${user.userName}"></td>
      </tr>
    </th:block>
  </tbody>
</table>

4、remove删除方法(tag包含标记的删除,但不删除它的子节点)

<table class="table">
  <thead>
    <tr th:remove="tag">
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <th:block th:each="user : ${users}">
      <tr>
        <td th:text="${user.userId}"></td>
        <td th:text="${user.userName}"></td>
      </tr>
    </th:block>
  </tbody>
</table>

5、all-but-first(删除所有包含标签的孩子,除了第一个)

<table class="table">
  <thead>
    <tr th:remove="all-but-first">
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <th:block th:each="user : ${users}">
      <tr>
        <td th:text="${user.userId}"></td>
        <td th:text="${user.userName}"></td>
      </tr>
    </th:block>
  </tbody>
</table>

6、none(什么也不做)

<table class="table">
  <thead>
    <tr th:remove="none">
      <th>用户ID</th>
      <th>用户名</th>
    </tr>
  </thead>
  <tbody>
    <th:block th:each="user : ${users}">
      <tr>
        <td th:text="${user.userId}"></td>
        <td th:text="${user.userName}"></td>
      </tr>
    </th:block>
  </tbody>
</table>

模版注释

1、注释可见

<!-- 你看的见我 -->

2、注释不可见

<!--/* 你看不见我 */-->

内联语法

使用内联语法显示文本

<div th:with="html='<h3>若依框架</h3>'">
	1、text标签显示
	<p><div th:text="${html}"></div></p>
	2、内联显示
	<p><div>[[${html}]]</div></p>
	3、utext标签显示
	<p><div th:utext="${html}"></div></p>
	4、内联显示
	<p><div>[(${html})]</div></p>
	5、关闭内联语法
	<p><div th:inline="none">[[${html}]]</div></p>
</div>

在javascript中使用内联语法

<script th:inline="javascript">
  var userName = [[${user.userName}]]
  // alert(userName);
</script>

在css中使用内联语法

<div th:with="color='red'">
  <style th:inline="css">
    .my-text {
      color: [[${color}]];
    }
  </style>
</div>

内联语法注释

<script th:inline="javascript">
  <!-- test1
  function test1{

  }
  !-->

  /*[+  test2
  function test2{

  }
  +]*/

  /*[- test3 */
  function test3{

  }
  /* -]*/
</script>

<style th:inline="css">
  <!-- test1 .test1 {
    color: [[${color}]];
  }
  !-->

	/*[+  test2
	.test2{
		color: [[${color}]]
	}
	+]*/
	
	/*[- test3 */   
	.test3 {
    color: [[${color}]];
  }
  /* -]*/
</style>

内联语法序列化

<script th:inline="javascript">
  // javabean
  var user = { id: 1, name: "张三" };
  //alert(user.name);
  // list
  var users = [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" },
  ];
  //alert(users.length);
  // map
  var map = { user1: { id: 1, name: "张三" }, user2: { id: 1, name: "李四" } };
  //alert(map.user1.name);
</script>

调用后台

html方式调用后台

<div>
  用户状态:<select
    name="status"
    th:with="type=${@dict.getType('sys_normal_disable')}"
  >
    <option value="">所有</option>
    <option
      th:each="dict : ${type}"
      th:text="${dict.dictLabel}"
      th:value="${dict.dictValue}"
    ></option>
  </select>
</div>

javascript方式调用后台

注意script需要添加th:inline="javascript"

<script th:inline="javascript">
  var datas = [[${@dict.getType('sys_normal_disable')}]];
  // alert(datas[0].dictValue);
</script>

自定义标签

在使用Thymeleaf模板引擎开发页面时,我们可以自定义一些通用的标签,来简化开发、降低代码量,下面我以开发中常见的下拉选为例,使用Thymeleaf自定义一个下拉选择字典数据的公共组件。

1、创建自定义标签构建类

package com.ruoyi.framework.thymeleaf.processor;

import java.util.ArrayList;
import java.util.List;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;
import org.unbescape.html.HtmlEscape;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.service.ISysDictTypeService;

/**
 * 自定义标签<dict:select>实现下拉框
 *
 * @author ruoyi
 */
public class SelectElementTagProcessor extends AbstractElementTagProcessor
{
    private static final String TAG_NAME = "select";
    private static final int PRECEDENCE = 1000;
    private ISysDictTypeService dictService = SpringUtils.getBean(ISysDictTypeService.class);

    public SelectElementTagProcessor(final String dialectPrefix)
    {
        super(TemplateMode.HTML, // 处理thymeleaf 的模型
                dialectPrefix, // 标签前缀名
                TAG_NAME, // 标签属性名
                true, // Apply dialect prefix to tag name
                null, // No attribute name: will match by tag name
                false, // No prefix to be applied to attribute name
                PRECEDENCE); // Precedence (inside dialect's own precedence)
    }

    @Override
    protected void doProcess(final ITemplateContext context, final IProcessableElementTag tag,
            final IElementTagStructureHandler structureHandler)
    {
        // html空格占位符
        List<String> options = new ArrayList<>();

        // 获取相关属性值
        String classValue = tag.getAttributeValue("class");
        final String id = tag.getAttributeValue("id");
        final String name = tag.getAttributeValue("name");
        final String style = tag.getAttributeValue("style");
        final Boolean headerKey = Boolean.parseBoolean(tag.getAttributeValue("headerKey"));
        final String headerValue = tag.getAttributeValue("headerValue");
        final String dict = tag.getAttributeValue("dict");
        if (StringUtils.isNotEmpty(dict))
        {
            List<SysDictData> dictDatas = dictService.selectDictDataByType(dict);
            for (SysDictData dictData : dictDatas)
            {
                StringBuilder option = new StringBuilder();
                option.append("<option value=\"");
                option.append(dictData.getDictValue());
                option.append("\">");
                option.append(dictData.getDictLabel());
                option.append("</option>");
                options.add(option.toString());
            }

            if (headerKey != null && headerValue != null)
            {
                StringBuilder defaultOption = new StringBuilder();
                defaultOption.append("<option value=\"").append(headerKey).append("\"");
                defaultOption.append(">").append(headerValue).append("</option>");
                options.add(0, defaultOption.toString());
            }
        }

        // 创建模型
        final IModelFactory modelFactory = context.getModelFactory();
        final IModel model = modelFactory.createModel();
        model.add(modelFactory.createText("\n\t"));
        // 添加模型元素
        IProcessableElementTag openElementTag = modelFactory.createOpenElementTag("select", "class", classValue);
        if (StringUtils.isNotEmpty(id))
        {
            openElementTag = modelFactory.setAttribute(openElementTag, "id", id);
        }
        if (StringUtils.isNotEmpty(name))
        {
            openElementTag = modelFactory.setAttribute(openElementTag, "name", name);
        }
        if (StringUtils.isNotEmpty(style))
        {
            openElementTag = modelFactory.setAttribute(openElementTag, "style", style);
        }
        model.add(openElementTag);
        model.add(modelFactory.createText("\n\t\t"));
        model.add(modelFactory.createText(HtmlEscape.unescapeHtml(String.join("\n\t\t", options))));
        model.add(modelFactory.createText("\n\t"));
        model.add(modelFactory.createCloseElementTag("select"));
        // 替换页面标签
        structureHandler.replaceWith(model, false);
    }
}

2、创建自定义标签注册类

package com.ruoyi.framework.thymeleaf.dialect;

import java.util.HashSet;
import java.util.Set;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.standard.StandardDialect;
import com.ruoyi.framework.thymeleaf.processor.SelectElementTagProcessor;

/**
 * 自定义字典标签
 *
 * @author ruoyi
 **/
public class DictDialect extends AbstractProcessorDialect
{

    /**
     * 定义方言名称
     */
    private static final String NAME = "Dict";

    /**
     * 定义方言属性
     */
    public static final String PREFIX = "dict";

    public DictDialect()
    {
        super(NAME, PREFIX, StandardDialect.PROCESSOR_PRECEDENCE);
    }

    @Override
    public Set<IProcessor> getProcessors(final String dialectPrefix)
    {
        final Set<IProcessor> processors = new HashSet<>();
        processors.add(new SelectElementTagProcessor(dialectPrefix));
        return processors;
    }
}

3、模板引擎加入配置

package com.ruoyi.framework.config;

import java.util.Collection;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templateresolver.ITemplateResolver;
import com.ruoyi.framework.thymeleaf.dialect.DictDialect;

/**
 * Thymeleaf配置.
 *
 * @author ruoyi
 */
@Configuration
public class ThymeleafConfig
{
    @Autowired
    private Collection<ITemplateResolver> templateResolvers = Collections.emptySet();

    @Autowired(required = false)
    private Collection<IDialect> dialects = Collections.emptySet();

    @Bean
    public SpringTemplateEngine templateEngine()
    {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        for (ITemplateResolver templateResolver : this.templateResolvers)
        {
            engine.addTemplateResolver(templateResolver);
        }
        for (IDialect dialect : this.dialects)
        {
            engine.addDialect(dialect);
        }
        engine.addDialect(new DictDialect());
        return engine;
    }
}

4、使用自定义标签,其中sys_normal_disable为字典的类型。

用户状态:<dict:select
  id="status"
  name="status"
  dict="sys_normal_disable"
  headerKey="*"
  headerValue="所有"
></dict:select>

多模块在ruoyi-framework加入依赖

<!-- 引入thymeleaf依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

自定义内置对象

Thymeleaf中提供了一些内置对象,并且在这些对象中提供了一些方法,方便我们来调用。获取这些对象,需要使用#对象名来引用。我们也可以自定义内置对象,下面使用Thymeleaf自定义时间工具内置对象。

1、实现IExpressionObjectFactory接口

package com.ruoyi.framework.thymeleaf.object;

import java.util.HashSet;
import java.util.Set;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.expression.IExpressionObjectFactory;
import com.ruoyi.common.utils.DateUtils;

public class DateUtilsObjectFactory implements IExpressionObjectFactory
{
    /**
     * 自定义内置对象名称
     */
    private static final String EXPRESSION_OBJECT_NAME = "dateUtils";

    /**
     * 返回表达式对象的名称
     */
    @Override
    public Set<String> getAllExpressionObjectNames()
    {
        Set<String> names = new HashSet<>();
        names.add(EXPRESSION_OBJECT_NAME);
        return names;
    }

    /**
     * 创建内置对象的应用的实例
     */
    @Override
    public Object buildObject(IExpressionContext context, String expressionObjectName)
    {
        if (EXPRESSION_OBJECT_NAME.equals(expressionObjectName))
        {
            return new DateUtils();
        }
        return null;
    }

    /**
     * 是否缓存该对象
     */
    @Override
    public boolean isCacheable(String expressionObjectName)
    {
        return false;
    }
}

2、定义内置对象的表达式方言

package com.ruoyi.framework.thymeleaf.object;

import org.thymeleaf.dialect.IExpressionObjectDialect;
import org.thymeleaf.expression.IExpressionObjectFactory;

/**
 * 自定义的时间工具对象
 *
 * @author ruoyi
 */
public class DateUtilsExpDialect implements IExpressionObjectDialect
{
    @Override
    public String getName()
    {
        return "DateUtils Dialect";
    }

    @Override
    public IExpressionObjectFactory getExpressionObjectFactory()
    {
        return new DateUtilsObjectFactory();
    }
}

3、内置对象添加到模板引擎配置

public class ThymeleafConfig
{
    ....

    @Bean
    public SpringTemplateEngine templateEngine()
    {
        ....
        engine.addDialect(new DateUtilsExpDialect());
        return engine;
    }
}

4、使用自定义内置对象调用方法,其中dateUtils为DateUtils.java类。

<span th:text="${#dateUtils.getDate()}"></span>
<span th:text="${#dateUtils.dateTimeNow('yyyy-MM-dd HH:mm:ss')}"></span>

多模块在ruoyi-framework加入依赖

<!-- 引入thymeleaf依赖 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

获取后台域对象值

request

request.setAttribute("requestParam", id);
<p th:xxxx="${requestParam}"></p>
var requestParam= '[[${requestParam}]]';

session

session.setAttribute("sessionParam", id);
<p th:xxxx="${session.sessionParam}"></p>
var sessionParam = '[[${session.sessionParam}]]';

application

application.setAttribute("applicationParam", id);
<p th:xxxx="${application.applicationParam}"></p>
var applicationParam = '[[${application.applicationParam}]]';

spring/model

mmap.put("modelParam", id);
<p th:xxxx="${modelParam}"></p>
var modelParam = '[[${modelParam}]]';

提示

取值时,推荐使用单引号或双引号包裹,避免当值为null时,前台的语法错误,如:var param = ;

常见问题

如何不登录直接访问

方法1:在ShiroConfig.java中设置filterChainDefinitionMap配置url=anon

	/admins/**=anon               # 表示该 uri 可以匿名访问
	/admins/**=auth               # 表示该 uri 需要认证才能访问
	/admins/**=authcBasic         # 表示该 uri 需要 httpBasic 认证
	/admins/**=perms[user:add:*]  # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
	/admins/**=port[8080]         # 表示该 uri 需要使用 8080 端口
	/admins/**=roles[admin]       # 表示该 uri 需要认证用户拥有 admin 角色才能访问
	/admins/**=ssl                # 表示该 uri 需要使用 https 协议
	/admins/**=user               # 表示该 uri 需要认证或通过记住我认证才能访问
	/logout=logout                # 表示注销,可以当作固定配置

	注意:
	anon,authcBasic,authc,user 是认证过滤器。
	perms,roles,ssl,rest,port 是授权过滤器。

方法2:在对应的方法上面使用@Anonymous注解。

// 方法定义匿名注解,作用于单独的方法
@Anonymous
@GetMapping("/list")
public List<SysXxxx> list(SysXxxx xxxx)
{
    return xxxxList;
}

如何更换项目包路径

懒人可以使用若依框架包名修改器 (opens new window)一键替换。

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.ruoyi.xxx换成com.xxxxx.xxx

提示

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

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

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

Constants.java 这个类com.ruoyi需要替换com.xxxx

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

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

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

ruoyi-system/resources/mapper/system/*
ruoyi-quartz/resources/mapper/quartz/*
ruoyi-generator/resources/mapper/generator/*
xml`包路径名称替换`com.xxxxx

6、更换pom文件内容

提示

以下pom.xml文件中包含ruoyi的关键字替换成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 引入了业务子系统
  • ruoyi-admin 添加业务子模块的依赖
  • ruoyi-xxxxx 新增业务模块pom检查配置是否正确

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

提示

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

IDEA更改页面不重启

经常有小伙伴问到这个问题,为什么我的用IDEA修改html页面之后不实时生效呢?

1、修改IDEA设置 File -> Settings -> Build Execution Deployment -> Build Project automatically 勾选

2、勾选Running Ctrl + Shift + Alt + / 然后选择 Registry,勾上 Compiler.autoMake.allow.when.app.running

PS:Eclipse开发工具无需任何配置。

如何使用多数据源如何使用多数据源

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

  1. 在 resources 目录下修改application-druid.yml
# 从库数据源
slave:
  # 开启从库
  enabled: true
  url: 数据源
  username: 用户名
  password: 密码
  1. 在Service实现中添加DataSource注解
@DataSource(value = DataSourceType.SLAVE)
public List<User> selectUserList()
{
    return mapper.selectUserList();
}

如果涉及到两个以上数据源,参考配置

如何更换主题皮肤

1、项目主页-个人信息中选择切换主题

2、修改主框架页-默认皮肤,在菜单参数设置修改参数键名sys.index.skinName支持如下几种皮肤

  • 蓝色 skin-blue
  • 绿色 skin-green
  • 紫色 skin-purple
  • 红色 skin-red
  • 黄色 skin-yellow

3、修改主框架页-侧边栏主题,在菜单参数设置修改参数键名sys.index.sideTheme支持如下几种主题

  • 深色主题theme-dark
  • 浅色主题theme-light

注:如需新增修改皮肤主题可以在skins.css中调整

提示

顶部默认主题颜色在skins.css

/** 蓝色主题 skin-blue **/
.navbar,
.skin-blue .navbar {
  background-color: #3c8dbc;
}

左侧默认主题颜色在static\css\style.css

.navbar-static-side {
  background: #2f4050;
}

nav .logo {
  background-color: #367fa9;
}

如何使用全局主题

skin.html页面新增调用更换主题样式方法

$("[data-skin]").on('click', function(e) {
    ....
    // 更换主题样式
	if($.isFunction(parent.switchThemeStyle)) {
		parent.switchThemeStyle();
	}
});

2、index.html,index-topnav.html页面新增iframe页全局更换方法

/** 更换主题样式 */
function switchThemeStyle() {
  for (var i = 0; i < $("iframe").length; i++) {
    var curTabWin = $("iframe")[i].contentWindow;
    if ($.isFunction(curTabWin.switchThemeStyle)) {
      curTabWin.switchThemeStyle();
    }
  }
}

3、include.html新增css节点和js方法控制

<!-- 通用CSS -->
<head th:fragment="header(title)">
  ....
  <link href="" id="skin_link" rel="stylesheet" />
</head>

<!-- 通用JS -->
<div th:fragment="footer">
  ....
  <script type="text/javascript">
    switchThemeStyle();
    function switchThemeStyle() {
      var link = document.getElementById("skin_link");
      link.setAttribute("href", "");
      // 皮肤缓存
      var skin = storage.get("skin");
      var linkStr = "/ruoyi/css/skin/";

      // 本地主题优先,未设置取系统配置
      var skinCss = "";
      if ($.common.isNotEmpty(skin)) {
        skinCss = skin.split("|")[0];
      } else {
        skinCss = "${sideTheme}";
      }
      linkStr = linkStr + skinCss + ".css?v=4.7.4";
      link.setAttribute("href", linkStr);
    }
  </script>
</div>

4、resources\static\ruoyi\css\skin新增对应的主题样式文件,对应主题颜色,例如skin-blue.css,skin-green.css,skin-purple.css,skin-red.css,skin-yellow.css

skin-blue.css文件示例,其他的文件主题也都是一样,只是颜色不一样。

/*按钮样式*/
.btn-primary {
  background-color: #1890ff;
  border-color: #1890ff;
}
.btn-primary:hover,
.btn-primary:active:hover,
.btn-primary:active {
  background-color: #40a9ff;
  border-color: #40a9ff;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
.btn-primary.active,
.open .dropdown-toggle.btn-primary {
  background-color: #1890ff;
  border-color: #1890ff;
}
.btn-outline.btn-primary {
  background-color: #ffffff;
  border-color: #1890ff;
  color: #1890ff;
}
.btn-outline.btn-primary:hover {
  color: #1890ff;
}
.btn-default {
  background-color: #ffffff;
  border-color: #d9d9d9;
}
.custom-table .btn-default .fa {
  color: #666666;
}
.custom-table .btn-default:hover .fa {
  color: #1890ff;
}
.btn-default:hover,
.btn-default:active:hover,
.btn-default:active {
  background-color: #ffffff !important;
  border-color: #1890ff !important;
  color: #1890ff !important;
}
.btn-outline.btn-default:hover {
  background-color: #ffffff;
  border-color: #40a9ff;
  color: #1890ff;
}

/*表格样式*/
.table-striped table > tbody > tr > td .btn-link {
  color: #1890ff;
}
.btn-link:hover,
.btn-link:focus,
.btn-link:active,
.btn-link.active,
.open .dropdown-toggle.btn-link {
  color: #1890ff;
}
.select2-container--bootstrap
  .select2-selection--multiple
  .select2-selection__choice {
  color: #fff;
  background: #1890ff !important;
  border: 1px solid #1890ff !important;
}
.select2-container--bootstrap
  .select2-results__option--highlighted[aria-selected] {
  background: #1890ff !important;
}
/*时间控件*/
.layui-laydate .layui-laydate-content .layui-this {
  background-color: #1890ff !important;
}
.switch-solid input:checked ~ span {
  background-color: #1890ff;
  border-color: #1890ff;
}
.text-info {
  color: #1890ff !important;
}
.custom-table .fixed-table-container .table td a.tooltip-show {
  color: #189dff;
}
/*表单input样式*/
.form-control:focus,
.select-list li input:focus,
.select-list li select:focus {
  border-color: #1890ff !important;
  box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/*弹出层按钮样式*/
.skin-blue .layer-ext-moon .layui-layer-btn .layui-layer-btn0 {
  background-color: #1890ff !important;
  border-color: #1890ff !important;
  font-size: 18px;
}

如何使用横向菜单

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

1、点击顶部最右侧个人中心头像,切换为横向菜单。(局部设置)

2、在参数管理设置主框架页-菜单导航显示风格,键值为topnav为顶部导航菜单。(全局设置)

如何获取用户登录信息)如何获取用户登录信息

  1. 第一种方法
// 获取当前的用户信息
User currentUser = ShiroUtils.getSysUser();
// 获取当前的用户名称
String userName = currentUser.getUserName();
  1. 第二种方法(子模块可使用)
// 获取当前的用户名称
String userName = (String) PermissionUtils.getPrincipalProperty("userName");

3、界面获取当前用户信息(支持任意th标签)

<input th:value="${@permission.getPrincipalProperty('userName')}" />

4、js中获取当前用户信息

var userName = [[${@permission.getPrincipalProperty('userName')}]];

如何防止请求重复提交

  1. 前端通过js控制
// 禁用按钮
$.modal.disable();
// 启用按钮
$.modal.enable();
  1. 后端通过@RepeatSubmit注解控制
/**
 * 在对应方法添加注解 @RepeatSubmit
 */
@RepeatSubmit
public AjaxResult editSave()

如何配置允许跨域访问

现在开发的项目一般都是前后端分离的项目,所以跨域访问会经常使用。

1、单个控制器方法CORS注解

@RestController
@RequestMapping("/system/test")
public class TestController {

    @CrossOrigin
    @GetMapping("/{id}")
    public AjaxResult getUser(@PathVariable Integer userId) {
        // ...
    }

	@DeleteMapping("/{userId}")
    public AjaxResult delete(@PathVariable Integer userId) {
        // ...
    }
}

2、整个控制器启用CORS注解

@CrossOrigin(origins = "http://ruoyi.vip", maxAge = 3600)
@RestController
@RequestMapping("/system/test")
public class TestController {

    @GetMapping("/{id}")
    public AjaxResult getUser(@PathVariable Integer userId) {
        // ...
    }

	@DeleteMapping("/{userId}")
    public AjaxResult delete(@PathVariable Integer userId) {
        // ...
    }
}

3、全局CORS配置(在ResourcesConfig重写addCorsMappings方法)

/**
 * web跨域访问配置
 */
@Override
public void addCorsMappings(CorsRegistry registry)
{
	// 设置允许跨域的路径
	registry.addMapping("/**")
			// 设置允许跨域请求的域名
			.allowedOrigins("*")
			// 是否允许证书
			.allowCredentials(true)
			// 设置允许的方法
			.allowedMethods("GET", "POST", "DELETE", "PUT")
			// 设置允许的header属性
			.allowedHeaders("*")
			// 跨域允许时间
			.maxAge(3600);
}

日期插件精确到时分秒

1、界面设置时间格式data-format,选择类型data-type属性。

<!-- data-type="date"(年)| data-type="month(月)| data-type="date"(日)| data-type="time"(时、分、秒)| data-type="datetime"(年、月、日、时、分、秒) -->
<li class="select-time">
  <label>创建时间: </label>
  <input
    type="text"
    class="time-input"
    placeholder="开始时间"
    name="params[beginTime]"
    data-type="datetime"
    data-format="yyyy-MM-dd HH:mm:ss"
  />
  <span>-</span>
  <input
    type="text"
    class="time-input"
    placeholder="结束时间"
    name="params[endTime]"
    data-type="month"
    data-format="yyyy-MM"
  />
</li>

2、通过js函数设置 datetimepicker日期控件可以设置format

$('.input-group.date').datetimepicker({
    format: 'yyyy-mm-dd hh:ii:ss',
    autoclose: true,
    minView: 0,
    minuteStep:1
});
laydate`日期控件可以设置`common.js` 配置`type=datetime
layui.use('laydate', function() {
	var laydate = layui.laydate;
	var startDate = laydate.render({
		elem: '#startTime',
		max: $('#endTime').val(),
		theme: 'molv',
		trigger: 'click',
		type : 'datetime',
		done: function(value, date) {
			// 结束时间大于开始时间
			if (value !== '') {
				endDate.config.min.year = date.year;
				endDate.config.min.month = date.month - 1;
				endDate.config.min.date = date.date;
			} else {
				endDate.config.min.year = '';
				endDate.config.min.month = '';
				endDate.config.min.date = '';
			}
		}
	});
	var endDate = laydate.render({
		elem: '#endTime',
		min: $('#startTime').val(),
		theme: 'molv',
		trigger: 'click',
		type : 'datetime',
		done: function(value, date) {
			// 开始时间小于结束时间
			if (value !== '') {
				startDate.config.max.year = date.year;
				startDate.config.max.month = date.month - 1;
				startDate.config.max.date = date.date;
			} else {
				startDate.config.max.year = '';
				startDate.config.max.month = '';
				startDate.config.max.date = '';
			}
		}
	});
});