codecamp

后台手册

分页实现

前端基于Bootstrap的轻量级表格插件 Bootstrap Table
后端分页组件使用Mybatis分页插件 PageHelper

分页实现流程

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

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

2、后台实现查询逻辑,调用startPage()方法即可自动完成服务端分页。

@PostMapping("/list")
@ResponseBody
public TableDataInfo list(User user)
{
    startPage();  // 此方法配合前端完成自动分页
    List<User> list = userService.selectUserList(user);
    return getDataTable(list);
}

注意:启动分页关键代码startPage()(只对该语句以后的第一个查询语句得到的数据进行分页)
如果改为其他数据库需修改配置application.yml helperDialect=你的数据库

导入导出

导入导出使用 Apache POI,目前支持参数如下

参数 类型 默认值 描述
name String 导出到Excel中的名字
dateFormat String 日期格式, 如: yyyy-MM-dd
readConverterExp String 读取内容转表达式 (如: 0=男,1=女,2=未知)
height String 14 导出时在excel中每个列的高度 单位为字符
width String 16 导出时在excel中每个列的宽 单位为字符
suffix String 文字后缀,如% 90 变成90%
defaultValue String 当值为空时,字段的默认值
prompt String 提示信息
combo String Null 设置只能选择不能输入的列内容
isExport String true 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写
targetAttr String 另一个类中的属性名称,支持多级获取,以小数点隔开
type Enum Type.ALL 字段类型(0:导出导入;1:仅导出;2:仅导入)

导出实现流程

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

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

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

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


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

3、在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、在实体变量上添加@Excel注解,默认为导出导入,也可以单独设置仅导入Type.IMPORT

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


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


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

3、在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);
}

上传下载

首先创建一张上传文件的表,例如:

drop table if exists sys_file;
create table sys_file (
  fileid        int(11)          not null auto_increment       comment '文件id',
  filename      varchar(50)      default ''                    comment '文件名称',
  filepath      varchar(255)     default ''                    comment '文件路径',
  primary key (fileid)
) engine=innodb auto_increment=200 default charset=utf8 comment = '文件表';

上传实现流程

1、参考示例代码。

function submitHandler() {
    if ($.validate.form()) {
        uploadFile();
    }
}


function uploadFile() {
    var formData = new FormData();
    if($('#file')[0].files[0] == null) {
        $.modal.alertWarning("请先选择文件路径");
        return false;  
    }
    formData.append('fileName', $("#fileName").val());
    formData.append('file', $('#file')[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);
        }
    });
}

2、在Controller添加对应上传方法

@Autowired
private ServerConfig serverConfig;

    
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(MultipartFile file, SysFile sysFile) throws IOException
{
    // 上传文件路径
    String filePath = Global.getUploadPath();
    // 上传并返回新文件名称
    String fileName = FileUploadUtils.upload(filePath, file);
    sysFile.setFilePath(fileName);
    return toAjax(sysFileService.insertSysFile(sysFile));
}

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

{
    title: '文件预览',
    formatter: function(value, row, index) {
        return '<a href="javascript:downloadFile(' + row.fileId + ')"><img style="width:30;height:30px;"  src="/profile/upload/' + row.filePath + '"/></a>';
    }
},

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

下载实现流程

1、参考示例代码。

function downloadFile(fileId){
    window.location.href = ctx + "system/sysFile/downloadFile/" + fileId;
}

2、在Controller添加对应上传方法

@GetMapping("/downloadFile/{fileId}")
public void downloadFile(@PathVariable("fileId") Integer fileId, HttpServletResponse response) throws Exception
{
    SysFile sysFile = sysFileService.selectSysFileById(fileId);
    String filePath = sysFile.getFilePath();
    String realFileName = sysFile.getFileName() + filePath.substring(filePath.indexOf("."));
    String path = Global.getUploadPath() + sysFile.getFilePath();
    response.setCharacterEncoding("utf-8");
    response.setContentType("multipart/form-data");
    response.setHeader("Content-Disposition", "attachment;fileName=" + realFileName);
    FileUtils.writeBytes(path, response.getOutputStream());
}

事务管理

在Spring Boot中,当我们使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。 所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。

例如:新增用户时需要插入用户表、用户与岗位关联表、用户与角色关联表。就可以使用事务让它实现回退。
做法非常简单,我们只需要在方法上添加@Transactional注解即可。事务可以用于ServiceController

@Transactional
public int insertUser(User user)
{
    // 新增用户信息
    int rows = userMapper.insertUser(user);
    // 新增用户岗位关联
    insertUserPost(user);
    // 新增用户与角色管理
    insertUserRole(user);
    return rows;
}

常见坑点1:遇到非检测异常时,事务开启,也无法回滚。 例如下面这段代码,账户余额依旧增加成功,并没有因为后面遇到检测异常而回滚!!

@Transactional
public void addMoney() throws Exception {
    //先增加余额
    accountMapper.addMoney();
    //然后遇到故障
    throw new SQLException("发生异常了..");
}

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

@Transactional(rollbackFor = Exception.class)
public void addMoney() throws Exception {
    //先增加余额
    accountMapper.addMoney();
    //然后遇到故障
    throw new SQLException("发生异常了..");
}

常见坑点2: 在业务层捕捉异常后,发现事务不生效。 这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。 例如:下面这段代码直接导致增加余额的事务回滚没有生效。

@Transactional
public void addMoney() throws Exception {
    //先增加余额
    accountMapper.addMoney();
    //谨慎:尽量不要在业务层捕捉异常并处理
    try {
        throw new SQLException("发生异常了..");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

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

@Transactional
public void addMoney() throws Exception {
    //先增加余额
    accountMapper.addMoney();
    //推荐:在业务层将异常抛出
    throw new RuntimeException("发生异常了..");
}

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,……}
....

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_REQUIRED。

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

异常处理

在日常开发中程序发生了异常,往往需要通过一个统一的异常处理,来保证客户端能够收到友好的提示。
通常情况下我们用try..catch..对异常进行捕捉处理,但是在实际项目中对业务模块进行异常捕捉,会造成代码重复和繁杂, 我们希望代码中只有业务相关的操作,所有的异常我们单独设立一个类来处理它。全局异常就是对框架所有异常进行统一管理 而这就表示在框架需要一个机制,将程序的异常转换为用户可读的异常。而且最重要的,是要将这个机制统一,提供统一的异常处理。 我们在可能发生异常的方法,全部throw抛给前端控制器;然后由前端控制器调用 全局异常处理器 对异常进行统一处理。 如此,我们现在的Controller中的方法就可以很简洁了。

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
}

这个代码示例写的非常浅显易懂,但是需要注意的是:基于@ControllerAdvice注解的全局异常统一处理只能针对于Controller层的异常,意思是只能捕获到Controller层的异常, 在service层或者其他层面的异常都不能捕获。

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

系统日志

在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,所有的异常我们单独设立一个注解来处理它。

在需要被记录日志的controller方法上添加@Log注解,使用方法如下:

@Log(title = "用户管理", businessType = BusinessType.INSERT) 

支持参数如下:

参数 类型 默认值 描述
title String 操作模块
businessType BusinessType BusinessType.OTHER 操作功能
operatorType OperatorType OperatorType.MANAGE 操作人类别
isSaveRequestData boolean true 是否保存请求的参数

逻辑实现代码 com.ruoyi.framework.aspectj.LogAspect
查询操作详细记录可以登录系统(系统管理-操作日志)

数据权限

在实际开发中,需要设置用户只能查看哪些部门的数据,一般称为数据权限
默认系统管理员admin拥有所有数据权限(userId=1)
在需要数据权限控制方法上添加@DataScope注解
@DataScope(tableAlias = "u"),其中u用来表示表的别名

/** 表的别名 */
String tableAlias() default "";

在mybatis查询标签中添加数据范围过滤 ${params.dataScope}

会生成如下关键代码:

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 )

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

多数据源

在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况
在需要切换数据源ServiceMapper方法上添加@DataSource注解
@DataSource(value = DataSourceType.MASTER),其中value用来表示数据源名称

/** 切换数据源名称 */
public DataSourceType value() default DataSourceType.MASTER;

注解实现数据源切换

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

手动实现数据源切换

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

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

注意:目前配置了一个从库,默认关闭状态。可新增多个从库,支持不同数据源(Mysql、Oracle、SQLServer)

代码生成

大部分项目里其实有很多代码都是重复的,几乎每个基础模块的代码都有增删改查的功能,而这些功能都是大同小异,如果这些功能都要自己去写,将会大大浪费我们的精力降低效率。所以这种重复性的代码可以使用代码生成。 1、修改代码生成配置
编辑resources目录下的generator.yml
author: # 开发者姓名,生成到类注释上
packageName: # 默认生成包路径
autoRemovePre: # 是否自动去除表前缀
tablePrefix: # 表前缀

2、新建数据库表结构(需要表注释)

drop table if exists sys_test;
create table sys_test (
  test_id           int(11)         auto_increment    comment '测试id',
  test_name         varchar(30)     default ''        comment '测试名称',
  primary key (test_id)
) engine=innodb auto_increment=1 default charset=utf8 comment = '测试表';

3、登录系统-系统工具 -> 代码生成
找到sys_test表,点击生成代码会得到一个ruoyi.zip
执行sql文件,覆盖文件到对应目录即可

所有代码生成的相关业务逻辑代码在ruoyi-generator模块,可以自行调整或剔除

定时任务

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

1、新建定时任务信息(系统监控 -> 定时任务)
任务名称:对应后台bean注解名称,如@Component("ryTask")
任务组名:对应的定时任务组名 随意填写。
方法名称:对应后台任务方法名称如ryParams
方法参数:对应后台任务方法名称值如ry,没有可不填。
执行表达式:可查询官方cron表达式介绍
执行策略:
立即执行(所有被misfire的执行会被立即执行,然后按照正常调度继续执行trigger。 9点和10点的被忽略掉,好像什么都没发生一样。下次执行将在11点被执行。) 执行一次(立即执行第一次misfire的操作,并且放弃其他misfire的(类似所有misfire的操作被合并执行了)。然后继续按调度执行。无论misfire多少次trigger的执行,都只会立刻执行1次 9点和10点的被合并执行一次(换句话说,10点需要执行的那次,被pass了)。下次执行将在11点被准时执行)
放弃执行(所有被misfire的执行都被忽略掉,调度器会像平时一样等待下次调度 9点和10点的执行(misfire的2个)被立即执行,下次执行将在11点被准时执行。)
并发执行:是否需要多个任务间同时执行
状态:是否启动定时任务
备注:描述信息

2、点击执行一次,测试定时任务是否正常及调度日志是否正确记录

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

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

项目介绍
前端手册
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }