# 后端手册

# 项目结构

包名 含义
com.eva.api 控制层/接口层
com.eva.biz 复合业务层
com.eva.service 颗粒业务层
com.eva.dao 持久层
com.eva.config 配置
com.eva.core 项目核心

如果我们划分得更细致些,那么可以得到下表

包名 含义
com.eva.api 控制层/接口层
com.eva.biz 复合业务层
com.eva.service 颗粒业务层
com.eva.service.aware 数据意识
com.eva.service.common 通用颗粒业务
com.eva.service.proxy 业务代理
com.eva.dao 持久层
com.eva.config 配置
com.eva.core 项目核心
com.eva.core.annotation 项目注解
com.eva.core.aware 通用意识接口、意识默认实现及意识辅助注解
com.eva.core.constants 常量定义
com.eva.core.exception 自定义异常
com.eva.core.model 通用模型
com.eva.core.utils 通用工具类

设计思想

项目为四层结构,api > biz > service > dao,相信很多新手会疑惑,但随着你技术、经验和思想的提升,你很快也会这么去做!现在让我们加快你成长的步伐,尝试着理解每一层的作用,为了更方便的理解,需要从后往前进行讲解!

  • 持久层

持久层用于将 处理好的数据保存至数据库。这一层的代码可直接生成,不需要手动编写。

  • 颗粒业务层

颗粒业务层用于 编写颗粒业务,例如怎样删除(逻辑删除还是物理删除),怎样批量删除(循环调用还是SQL实现)等。在这一层中不涉及多表操作(除查询外)。这一层的代码可直接生成,不需要手动编写。

  • 复合业务层

复合业务层用于 编写复合业务,这一层可能涉及到多表的增删改操作,而单表的增删改操作的业务实现在颗粒业务层。所以复合业务层应该使用颗粒业务层对象,而不应该直接使用持久层对象。当然,如果你的业务足够简单,例如删除一条单表记录,那么这一层可以不用实现。如果你的业务是一个复合业务,例如新建时需要验证编码字段是否重复,那么此时需要在该层添加创建方法,并在该方法中进行验证。

  • 控制层/接口层

控制层/接口层用于 定义接口并调用相关业务方法,而业务方法可以来自复合业务层和颗粒业务层。

# 服务端口配置

端口配置在application.yml的server.port属性中。调整后需重启服务。

# 权限控制

Eva整合Shiro实现权限控制,通过Shiro提供的@RequiresRoles@RequiresPermissions注解可完成角色或权限的控制。

# @RequiresRoles

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

  • value: 角色列表
  • logical: 角色之间的判断关系,默认为Logical.AND

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

 




@RequiresRoles("admin")
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

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

 




@RequiresRoles({"admin", "manager"})
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

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

 




@RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR)
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

# @RequiresPermissions

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

  • value: 权限列表
  • logical: 权限之间的判断关系,默认为Logical.AND

示例1: 以下代码表示必须拥有order:create权限才可访问

 




@RequiresPermissions("order:create")
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

示例2: 以下代码表示必须拥有order:create order:update权限才可访问

 




@RequiresPermissions({"order:create", "order:update"})
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

示例3: 以下代码表示必须拥有order:create order:update角色才可访问

 




@RequiresPermissions(value = {"order:create", "order:update"}, logical = Logical.OR)
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

# 参数验证

Eva采用spring自带的@Validated注解进行参数验证。为了在使用实体类的情况下更好的区分出新建、编辑和其他操作验证的不同,Eva提供了OperaType操作类型标识类。使用方式如下

MyController.java


 




 



// 新建
public ApiResponse create(@Validated(OperaType.Create.class) @RequestBody MyModel myModel) {
    return ApiResponse.success(...);
}

// 编辑
public ApiResponse updateById(@Validated(OperaType.Update.class) @RequestBody MyModel myModel) {
    return ApiResponse.success(...);
}
1
2
3
4
5
6
7
8
9

MyModel.java




 



 




public class MyModel {
   
   // 仅在编辑时验证
   @NotNull(message = "主键不能为空", groups = {OperaType.Update.class})
   private Integer id;
   
   // 在新建和编辑时验证
   @NotBlank(message = "名称不能为空", groups = {OperaType.Create.class, OperaType.Update.class})
   private String name;
}

1
2
3
4
5
6
7
8
9
10
11

扩展

OperaType类只是用于标识出不同的操作类型,所以如果你有更多的操作类型,你可以在OperaType类中继续添加新的标识。

# 无认证接口实现

有些时候我们允许不登录直接访问接口,此时需要在拦截器中配置接口放行。如下

com.myproject.WebConfigurer.java
























 




@Configuration
public class WebConfigurer implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(
                        "/resource/result/**", 
                        "/resource/image/**", 
                        "/resource/result", 
                        "/resource/record", 
                        "/sms/**", 
                        "/doc.html", 
                        "/swagger-resources/**", 
                        "/webjars/**", 
                        "/user/regis", 
                        "/user/login", 
                        "/user/setPwd", 
                        "/user/findPwd",
                        "/new/interface"
                );
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

注意

系统中提供的需要认证才能访问的接口,请勿在此放行。这样接口获取不到用户信息导致出现错误。

# 事务处理

在需要进行事务处理的方法上添加@Transactional注解即可。无论是service层还是biz层这种方式都是有效的。

误区

有些同学可能因为"懒"的缘故,直接在Controller中实现biz层做的事情,无论Controller方法上添加@Transactional是否能使事务生效,这种做法都是不规范的。

# 分页实现

虽然分页代码可以直接生成,但我们仍然需要为您说明分页的实现逻辑。

# 单表的分页实现

单表的分页使用MyBatis Plus自带的分页方法来实现是再好不过的选择了,这样可方便的实现多条件查询。

public PageData<MyModel> findPage(PageWrap<MyModel> pageWrap) {
    IPage<MyModel> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
    QueryWrapper<MyModel> queryWrapper = new QueryWrapper<>(WrapperUtil.blankToNull(pageWrap.getModel()));
    return PageData.from(myMapper.selectPage(page, queryWrapper));
}
1
2
3
4
5

# 多表的分页实现

多表的分页需要编写SQL语句,使用MyBatis的分页插件来实现会是一个明智的选择。

@Override
public PageData<MyListVO> findPage(PageWrap<MyQueryDTO> pageWrap) {
    PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
    List<MyListVO> myList = myMapper.selectManageList(pageWrap.getModel());
    return PageData.from(new PageInfo<>(myList));
}
1
2
3
4
5
6

PageData和PageWrap

细心的同学可能已经发现,无论是单表分页还是多表分页,方法参数均为PageWrap,方法返回均为PageData,为什么不使用MyBatis Plus自带的IPage?有经验的同学可能不难理解这是一种包装,这样可以方便的扩展我们想要的字段,并且无论分页方式如何变化,方法的请求参数和响应结构都能始终保持一致。

# 分页字段排序的实现

有时候我们需要实现列表中存在按自定义列排序的需求。Eva做了些简单的封装支持了字段排序的功能。

# 单表的分页字段排序

单表的分页字段排序可以使用MyBatis Plus默认的实现来处理。


 

 
 
 
 
 
 
 
 



public PageData<MyModel> findPage(PageWrap<MyModel> pageWrap) {
    IPage<MyModel> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
    QueryWrapper<MyModel> queryWrapper = new QueryWrapper<>(MyBatisPlus.blankToNull(pageWrap.getModel()));
    // 字段排序
    for(PageWrap.SortData sortData: pageWrap.getSorts()) {
        if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
            queryWrapper.orderByDesc(sortData.getProperty());
        } else {
            queryWrapper.orderByAsc(sortData.getProperty());
        }
    }
    return PageData.from(myMapper.selectPage(page, queryWrapper));
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 多表的分页字段排序

多表的排序需要结合新的方法参数和SQL处理来完成,不过实现也非常简单。

MyMapper.java

增加orderByClause,并通过@Param注解标记SQL参数名称。

public interface MyMapper extends BaseMapper<MyModel> {

    List<MyModelListVO> selectManageList(@Param("dto") QueryMyModelDTO dto, @Param("orderByClause") String orderByClause);
}
1
2
3
4

MyMapper.xml










 



 



<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="...">
  <select id="selectManageList" ...>
    SELECT ...
    FROM ...
    LEFT JOIN ...
    LEFT JOIN ...
    <where>
      <if dto.param != null>
         AND ...
      </if>
    </where>
    ${orderByClause}
  </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

MyServiceImpl.java




 



@Override
public PageData<MyListVO> findPage(PageWrap<MyQueryDTO> pageWrap) {
    PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
    List<MyListVO> myList = myMapper.selectManageList(pageWrap.getModel(), pageWrap.getOrderByClause());
    return PageData.from(new PageInfo<>(myList));
}
1
2
3
4
5
6

注意

MyMapper.java中使用@Param注解标识了SQL参数的名称,条件参数对象被标识为dto,因此在使用条件字段时应使用dto.prop(prop为字段名称),而不应直接使用prop

# 导出Excel

在Eva中实现一个导出功能是非常简单的。如下

# 1. 定义导出接口

public void exportExcel (HttpServletResponse response) {
   // 使用ExcelExporter导出文件 
   ExcelExporter.build(MyModel.class).export(myModelService.findList(), "My Excel", response);
   // 你还可以指定Sheet的名称
   // ExcelExporter.build(MyModel.class).export(myModelService.findList(), "My Excel", "My Sheet", response);
}
1
2
3
4
5
6

# 2. 配置Excel列信息

文件:MyModel.java

public class MyModel {

    @ExcelColumn(name="列名1")
    private String name;
    
    @ExcelColumn(name="列名2")
    private String name2;
}
1
2
3
4
5
6
7
8

这样我们就完成了导出Excel接口的实现。

# ExcelColumn注解参数说明

参数 说明
name 列名
width 列宽
index 列排序,默认为-1(按字段定义顺序)
align 对齐方式
backgroundColor 列头背景色
dataBackgroundColor 数据单元格的背景色
color 字体颜色
fontSize 字体大小
bold 是否加粗
italic 是否倾斜
valueMapping 值映射,如0=女;1=男
prefix 数据前缀
suffix 数据后缀
dateFormat 日期格式,只有数据为java.util.Date时才生效
handler Class, 自定义数据处理器
args String[], 自定义数据处理器的额外参数

# 自定义数据处理器

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

  • 1. 指定自定义的数据处理器
public class MyModel {

    @ExcelColumn(name="列名1", handler=MyDataHandler.class, args = {"arg1", "arg2"})
    private String name;
}
1
2
3
4
5
  • 2. 编写数据处理器MyDataHandler
public class MyDataHandler implements ExcelDataHandlerAdapter {
    
    @Override
    Object format (Object... args) {
        // 其中args[0]为单元格的数据,arg[1]及其之后为@ExcelColumn的args参数(在这里args[1]的结果为"arg1")
    }
} 
1
2
3
4
5
6
7

# 自定义数据权限

默认情况下,Eva实现了部门数据权限和独立的岗位数据权限控制。如果您需要添加新的数据权限,如审批数据权限,物品数据权限等,那么您也可以清晰且方便的实现。实现一套数据权限的步骤如下:

# 1. 定义业务模块和权限类型

文件:core/DataPermissionConstants.java

/**
 * 数据权限模块
 */
@Getter
@AllArgsConstructor
enum Module {
    MY_MODULE("MY_MODULE", "我的模块"),
    ;
    
}

/**
 * 数据权限类型
 */
@Getter
@AllArgsConstructor
enum Type {
    ALL((short)0,"全部", new Module[]{}),
    MY_MODULE_CUSTOM((short)1001, "自定义数据", new Module[]{Module.MY_MODULE}),
    MY_MODULE_CHILD((short)1002, "用户所属及其子数据", new Module[]{Module.MY_MODULE}),
    ;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2. 编写数据意识类继承DefaultDataPermissionAware

@Component
public class MyModelDataPermissionAware extends DefaultDataPermissionAware<MyModel> {
    
    /**
     * 覆盖父类方法:返回当前数据权限的模块
     */
    @Override
    public DataPermissionConstants.Module module() {
        return DataPermissionConstants.Module.MY_MODULE;
    }

    /**
     * 覆盖父类方法:当角色未配置数据权限时的默认数据权限。
     */
    @Override
    public List<SystemDepartmentListVO> defaultData(Integer userId) {
        return child(userId);
    }
    
    /**
     * 所有权限
     */
    @DataPermissionMapping(value = DataPermissionConstants.Type.ALL, priority = 1)
    public List<SystemDepartmentListVO> all() {
        // 数据权限为"全部"时的数据获取逻辑。
    }
    
    /**
     * 自定义数据
     */
    @DataPermissionMapping(value = DataPermissionConstants.Type.MY_MODULE_CUSTOM, priority = 2, injectCustomData = true)
    public List<MyModel> custom (String customData) {
        // 数据权限为"自定义数据"时的数据获取逻辑。
    }
    
    /**
     * 用户所属及其子数据
     */
    @DataPermissionMapping(value = DataPermissionConstants.Type.MY_MODULE_CUSTOM, priority = 3, injectUser = true)
    public List<MyModel> child (Integer userId) {
        // 数据权限为"用户所属及其子数据"时的数据获取逻辑。
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 3. 调用数据权限意识

DefaultDataPermissionAware类提供了execute方法,该方法将自动根据用户角色配置的数据权限执行权限方法。


@Autowired
private MyModelDataPermissionAware myModelDataPermissionAware;

public List<MyModel> findList () {
    return myModelDataPermissionAware.excute();
}
1
2
3
4
5
6
7

# @DataPermissionMapping说明

@DataPermissionMapping注解用于标记和配置数据权限处理方法。他有如下参数:

参数 说明
value 权限类型
priority 优先级,不允许重复
injectUser 是否注入用户ID参数,为true时方法可接收Integer userId参数
injectCustomData 是否注入自定义数据参数,为true时方法可接收String customData参数

priority/优先级的作用

一个用户可能拥有多个角色,多个角色意味着可能拥有不同的数据权限。priority用于在一个用户拥有多种数据权限时该如何选择使用哪种数据权限,值越小,优先级越高。

# 防重复提交

在接口方法上添加@PreventRepeat注解即可,他有如下参数:

# @PreventRepeat

参数 说明
value 防重复规则设定类,默认为PreventRepeatDefaultHandler.class
interval 间隔时间,小于此时间视为重复提交,默认为800毫秒
message 错误消息
limit 1分钟内限制的请求次数(<=0时表示不限制),默认为0
limitMessage 被限制时的错误消息
lockTime 超出请求限制次数时锁定的时长(ms),默认10分钟

示例1:采用默认参数

 




@PreventRepeat
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

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

 




@PreventRepeat(interval = 1000, message = "请求过于频繁")
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

示例3:指定接口时限调用次数和消息

 




@PreventRepeat(limit = 10, limitMessage = "请求过于频繁,请休息10分钟以后再试")
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

示例4:指定自定义防重复规则实现类

 




@PreventRepeat(value = MyPreventRepeatHandler.class ,interval = 1000, message = "请求过于频繁")
public ApiResponse create(...) {
    return ApiResponse.success(...);
}
1
2
3
4

# 自定义防重复提交规则

默认情况下防重复的规则由PreventRepeatDefaultHandler.class设定,它将请求路径、请求体参数、Cookie信息合并在一起并签名作为请求的Key。如果你并不希望这样,那么你可以自定义防重复的规则。实现方式如下

@Component
public class MyPreventRepeatHandler extends PreventRepeatAdapter {

    @Override
    public String sign(HttpServletRequest request) {
        // 根据您要验证的参数进行签名并返回即可
    }
}
1
2
3
4
5
6
7
8

# 操作日志

Eva的操作日志分为两种模式——智能模式和手动模式。您可以在构建项目时选择你喜欢的模式,当然我们推荐智能模式,因为它的确非常方便。无论是手动模式还是智能模式,Eva都通过@Trace注解来描述日志内容。

# @Trace

@Trace注解可以添加在接口方法上,也可以添加到Controller类上。以下是注解参数说明。

参数 含义 默认值 说明
module 模块名称 不存在值时读取类上@Trace的module参数,如果类上无@Trace则读取Swagger @Api的tags参数
type 操作类型 TraceType.AUTO 默认自动推测
remark 操作备注 不存在值时读取Swagger @ApiOperation,如果不存在@ApiOperation,则读取操作类型的备注
exclude 是否排除 false 为true时表示不为接口添加日志记录
withRequestParameters 是否写入请求参数 true 为false时表示日志中不记录请求参数
withRequestResult 是否写入请求结果 true 为false时表示日志中不记录请求结果

# 智能模式

智能模式下,默认会为所有的非查询接口添加操作日志。并且会根据接口路径来自动识别操作类型。在一个管理系统中通常所有的数据流转操作都需要记录日志,所以我们推荐您使用智能模式来实现操作日志的功能。为了更好的管理不记录操作日志的情况,Eva提供了注解排除方式和路径排除方式。注解排除方式即@Trace(exclude = true),路径排除方式则通过配置文件处理,如下:

application.yml

trace:
  # 开启智能跟踪模式
  smart: true
  # 排除跟踪的URL正则
  exclude-patterns: .*/list$, .*/tree$, .*/page$
1
2
3
4
5

约定

智能模式自动识别的原理是通过正则匹配接口路径,为了能够准确的识别,你应该按如下约定进行接口路径的定义:

  • 新增: 满足正则表达式.+/create.*
  • 修改: 满足正则表达式.+/update.**
  • 删除: 满足正则表达式.+/delete.*
  • 批量删除: 满足正则表达式.+/delete/batch$
  • 导入: 满足正则表达式.+/import.*
  • 导出: 满足正则表达式.+/export.*
  • 重置: 满足正则表达式.+/reset.**

# 手动模式

手动模式下,接口必须添加@Trace注解才会使接口记录日志。

不要随意切换

由于智能模式和手动模式在使用时刚好是反向作用,所以建议您在项目初期就定好使用哪种模式。在项目后期不要随意切换,防止操作日志的丢失。

# 实用工具类

所有工具类都通过Utils调用,这样你可以不用去查找工具类就可以方便的获取工具类对象。

# Utils.Http

Utils.Http为Http请求工具类,我们在设计时采用了链式调用的方案,这样可以增强代码可读性,并且使用起来也更加方便。在Eva中,发起一次请求的基本逻辑如下:

  1. 构造Http.HttpWrap对象
  2. 请求设置(如设置请求头,请求超时时间等)
  3. 发起请求
  4. 请求结果转换

构造Http.HttpWrap对象

方法名称 参数 方法返回 说明
build String url Http.HttpWrap 根据请求地址构造
build String url, String charset Http.HttpWrap 根据请求地址和编码构造

请求设置

方法名称 参数 方法返回 说明
setRequestProperty String key, String value Http.HttpWrap 设置请求头
setConnectTimeout int timeout Http.HttpWrap 设置连接超时时间
setReadTimeout int timeout Http.HttpWrap 设置读取超时时间
gzip Http.HttpWrap 开启gzip压缩

发起请求

方法名称 参数 方法返回 说明
get Http.HttpResult 发起GET请求
post Http.HttpResult 发起POST请求
post String params Http.HttpResult 发起POST请求
postJSON Map<String, Object> paramsMap Http.HttpResult 发起POST请求
postJSON JSONObject paramJSONObject Http.HttpResult 发起POST请求

请求结果转换

方法名称 参数 方法返回 说明
toStringResult String 转为字符串
toJSONObject JSONObject 转为fastjson JSONObject对象
toClass Class 转为指定class后的对象 转为指定类对象

示例1: 发起GET请求

Utils.Http.build(url)
    .get()
    .toStringResult();
1
2
3

示例2: 发起POST请求

Utils.Http.build(url)
    .post(data)
    .toStringResult();
1
2
3

示例3: 开启gzip压缩

Utils.Http.build(url)
    .gzip()
    .post(data)
    .toStringResult();
1
2
3
4

示例4: 设置请求编码

Utils.Http.build(url, Charset.forName("GBK").toString())
    .get()
    .toStringResult();
1
2
3

# Utils.OSS

Utils.OSS为OSS工具类,用于处理文件的上传与下载,跟Utils.Http一样,Utils.OSS也采用了链式调用设计。它有如下方法:

方法名称 参数 方法返回 说明
setMaxSize int Utils.OSS 设置文件大小限制,单位为M
setFileTypes String Utils.OSS 设置文件类型限制(多个类型使用","隔开,如".jpg,jpeg,.png")
uploadImage MultipartFile OSS.UploadResult 上传图片
uploadImage MultipartFile, String OSS.UploadResult 上传图片,第二个参数为业务路径,如使用"avatar"表示用户头像路径,"goods/cover"表示商品封面图片等
upload MultipartFile OSS.UploadResult 上传文件
upload MultipartFile, String OSS.UploadResult 上传文件,如使用"contract"表示合同文件,"contract/attach"表示合同附件
download String InputStream 下载文件

可以看到,所有上传的方法均返回OSS.UploadResult对象,这样可以方便我们获取文件相关的重要内容。

OSS.UploadResult属性说明

属性名称 类型 说明
originalFilename String 源文件名称
fileKey String 文件的key
accessUri String 访问路径/下载路径,默认情况下图片访问路径为/resource/image,文件下载路径为/resource/attach

示例1:文件大小和格式的限制

// 上传文件"avatar.jpg"
Utils.OSS
    // 限定文件大小不超过5M
    .setMaxSize(5)
    // 限定只允许.png和.jpg格式文件的上传
    .setFileTypes('.png,.jpg')
    .uploadImage(file);
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "avatar.jpg", accessUri: "/resource/image?f=avatar.jpg" }
1
2
3
4
5
6
7
8

示例2:指定业务路径

// 上传文件"avatar.jpg"
Utils.OSS.uploadImage(file, "avatar");
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "avatar/avatar.jpg", accessUri: "/resource/avatar?f=avatar/avatar.jpg" }
1
2
3

下载/预览文件的特殊处理 & 业务路径

您可能会遇见在下载/预览文件时需要对文件特殊处理的场景,如为图片或PDF添加水印,下载时添加下载时间等。此时我们应该通过业务路径将其在业务上进行区分(这也是业务路径的主要用途),如上传文件时设置业务路径为contract(标识为合同文件),那么合同文件的下载路径将变为/resource/contract,我们为此实现接口即可。具体实现可参考FileAccessController.java类。

# Utils.Date

Utils.Date为日期工具类,它有如下方法:

方法名称 参数 方法返回 说明
getStart java.util.Date date java.util.Date date 获取日期的开始时间
getEnd java.util.Date date java.util.Date date 获取日期的结束时间

示例

// 返回时分秒毫秒均为0的当前日期对象
Utils.Date.getStart(new Date());

// 返回时分秒毫秒均为0的下一天日期对象
Utils.Date.getEnd(new Date());
1
2
3
4
5

# Utils.Location

Utils.Location为地区工具类,它有如下方法:

方法名称 参数 方法返回 说明
getLocation String ip Location.Info 根据IP获取地区信息
getLocationString String ip Location.Info 根据IP获取地区简要

# Utils.UserClient

Utils.UserClient为用户客户端信息工具类,它有如下方法:

方法名称 参数 方法返回 说明
getOS HttpServletRequest request String 获取客户端操作系统信息
getBrowser HttpServletRequest request String 获取客户端浏览器信息
getIP HttpServletRequest request String 获取客户端IP
getPlatform HttpServletRequest request String 获取用户操作的平台

# Utils.Server

Utils.Server为服务器信息工具类,它有如下方法:

方法名称 参数 方法返回 说明
getIP String 获取当前服务器IP(局域网)
getMAC String 获取当前服务器MAC地址

# Utils.Monitor

Utils.Monitor为服务监听工具类,它有如下方法:

方法名称 参数 方法返回 说明
current Monitor 获取当前时刻的监听信息

# Utils.MyBatisPlus

Utils.MyBatisPlus为MyBatis Plus工具类,它有如下方法:

方法名称 参数 方法返回 说明
blankToNull T object T 将对象中所有的空字符串转为null

提示

相信很多人会疑惑,blankToNull方法应该跟MyBatis Plus没关系才对。的确,但Eva期望MyBatis plus中的blankToNull是独立的,因为它可能有别于常见的blankToNull方法的处理逻辑。或许,在Eva 2.0版本将调整它的名称。

# Utils.Secure

Utils.Secure为安全处理工具类,它有如下方法:

方法名称 参数 方法返回 说明
encryptPassword String password, String salt String 根据密码和密码盐加密密码

# 响应状态定义及规范

系统的响应状态定义在ResponseStatus枚举中,如下

@Getter
@AllArgsConstructor
public enum ResponseStatus {
    // 400开头表示参数错误
    BAD_REQUEST(4000, "参数错误"),
    DATA_EMPTY(4001, "找不到目标数据"),
    DATA_EXISTS(4002, "记录已存在"),
    PWD_INCORRECT(4003, "密码不正确"),
    VERIFICATION_CODE_INCORRECT(4004, "验证码不正确或已过期"),
    ACCOUNT_INCORRECT(4005, "账号或密码不正确"),
    // 510开头表示可能导致数据错误的异常
    DUPLICATE_SUBMIT(5100, "请勿重复提交"),
    NOT_ALLOWED(5101, "不允许的操作"),
    ;

    private int code;

    private String message;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

响应码如何定义对程序不会产生直接影响,但我们希望您在使用Eva时可以培养一个好的习惯——定义可识别的响应码。除了目前定义的响应码外,下面再给出一些例子供参考

响应码前缀 说明
10 以10开头表示用户模块错误,如1000表示登录过于频繁
11 以11开头表示会员模块错误,如1100表示会员已到期
12 以12开头表示订单模块错误,如1200表示货源不足
400 以400开头表示参数错误所导致的各类情况的响应码
500 以500开头表示程序因异常终止,不同类型的异常对应不同的响应码
510 以510开头表示请求可能导致数据错误

您可以按照您团队和业务的考量将响应码以其它方式规范化。这样的一个显而易见的好处是,我们仅凭响应码就能识别错误的严重性。

# 自定义全局异常处理

全局异常处理在类GlobalExceptionHandler中实现,通过spring的@RestControllerAdvice注解实现。如果您需要全局捕获某异常,可以参考以下代码来增加处理。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 业务异常处理
     */
    @ExceptionHandler(BusinessException.class)
    public <T> ApiResponse<T> handleBusinessException (BusinessException e) {
        log.error(e.getMessage(), e);
        return ApiResponse.failed(e.getCode(), e.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

无法捕获异常?

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

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

# 业务异常处理

Eva为业务异常封装了异常对象BusinessException,通过四个构造方法可方便的构造业务异常对象。定义如下

@Data
public class BusinessException extends RuntimeException {

    private Integer code;

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(Integer code, String message, Throwable e) {
        super(message, e);
        this.code = code;
    }

    public BusinessException(ResponseStatus status) {
        super(status.getMessage());
        this.code = status.getCode();
    }

    public BusinessException(ResponseStatus status, Throwable e) {
        super(status.getMessage(), e);
        this.code = status.getCode();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 登录令牌规则 & 安全验证

登录令牌对于一个系统来说十分重要,因为它标识了是谁在访问系统。所以,您可能需要更为复杂的登录令牌生成规则,并希望可以对令牌进行安全验证。Eva在ShiroDefaultTokenManager类中实现了令牌的生成和验证。实现非常简单,如下

@Component
public class ShiroDefaultTokenManager {

    public String build() {
        return UUID.randomUUID().toString();
    }

    public void check(String token) throws UnSafeSessionException {
        if (token == null || token.length() != 36) {
            throw new UnSafeSessionException();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

您可以直接修改此类来完成令牌的生成和校验。

# 登录保留时长/会话过期时长

可通过application.yml中的cache.session.expire控制,单位为秒。

# 验证码过期时长

可通过application.yml中的cache.captcha.expire控制,单位为秒。

# Swagger的启用和禁用

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

记得关闭

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

# Redis高可用配置

在Eva中,Redis的高可用采用主从 + 哨兵模式方案。至于是否需要读写分离,可以根据您的项目需要来更改配置。下面详细的说明如何进行配置。

Redis版本:2.8.17

我将采用一台机器启动三台Redis服务的方式来模拟实际情况下的多台机器。因为无论是一台机器还是多台机器,他们的配置是一样的。所以我将Redis原解压文件拷贝了三份,得到了如下目录:

  • master:主服务redis
  • 7001:从服务redis
  • 7002:从服务redis

每个目录下的内容目前都是一致的(都是原redis解压后的文件)。现在让我们进行主从服务的配置。

# 配置主服务master

文件:master/redis.conf

port 7000 # 主服务端口
requirepass <master-password> # 主服务密码(可为空,安全起见,在生产环境上必须设置)
1
2

# 配置从服务7001

文件:7001/redis.conf

port 7001 # 端口号
slaveof localhost 7000 # 设置本服务为主服务localhost 7000的从服务
slave-read-only yes # 设置从服务只读(读写分离时需设置此项为yes)
masterauth <master-password> # 主服务密码(如果主服务存在密码的话需要设置此项)
1
2
3
4

# 配置从服务7002

文件:7002/redis.conf

port 7002 # 端口号
slaveof localhost 7000 # 设置本服务为主服务localhost 7000的从服务
slave-read-only yes # 设置从服务只读(读写分离时需设置此项为yes)
masterauth <master-password> # 主服务密码(如果主服务存在密码的话需要设置此项)
1
2
3
4

这样,主从服务就配置完成了。下面我们还需要配置哨兵,以实现在主服务宕机时自动选举一个从服务为主服务,避免在读写分离时因主服务宕机而无法继续写入缓存。通常情况下,我们应该为每个服务节点配置一个哨兵。

# 配置主服务master哨兵

文件:master/sentinel.conf

port 27000
sentinel monitor mymaster 127.0.0.1 7000 2
1
2

# 配置从服务7001哨兵

文件:7001/sentinel.conf

port 27001
sentinel monitor mymaster 127.0.0.1 7000 2
1
2

# 配置主服务7002哨兵

文件:7002/sentinel.conf

port 27002
sentinel monitor mymaster 127.0.0.1 7000 2
1
2

其中127.0.0.1 7000表示主服务,最后的数字2为超过多少个哨兵认为主服务宕机时可以决定主服务宕机的常数。在我们的配置中,表示超过2个哨兵认为主服务宕机了就可以确定主服务确实宕机了。通常情况下这个数字不宜太高,当所需的服务不多时,此配置应该要大于服务总数的1/2,但如果所需的服务足够多,则至少需要大于服务总数的1/3

到这里,服务和哨兵都配置完成了。我们需要相继启动服务和哨兵,如下

启动所有redis服务

cd master
./src/redis-server redis.conf

cd 7001
./src/redis-server redis.conf

cd 7002
./src/redis-server redis.conf
1
2
3
4
5
6
7
8

启动所有哨兵

cd master
./src/redis-sentinel sentinel.conf

cd 7001
./src/redis-sentinel sentinel.conf

cd 7002
./src/redis-sentinel sentinel.conf
1
2
3
4
5
6
7
8

现在,你可以对主从同步、自动选举进行测试了,注意留意redis服务和哨兵的日志,相信你很快就会明白。