# 🇨🇳后端手册
# 项目结构
包名 | 含义 |
---|---|
com.eva.api | 控制层/接口层 |
com.eva.biz | 业务层 |
com.eva.service | 基础服务层 |
com.eva.dao | 持久层 |
com.eva.core | 项目核心 |
如果我们划分得更细致些,那么可以得到下表
包名 | 含义 |
---|---|
com.eva.api | 控制层/接口层 |
com.eva.biz | 业务层 |
com.eva.service | 基础服务层 |
com.eva.service.system | 系统管理,系统运维基础服务 |
com.eva.service.common | 通用基础服务 |
com.eva.dao | 持久层 |
com.eva.core | 项目核心 |
com.eva.core.authorize | 授权 |
com.eva.core.cache | 缓存 |
com.eva.core.config | 第三方框架、异常等配置 |
com.eva.core.constants | 常量定义 |
com.eva.core.excel | 导入导出Excel |
com.eva.core.exception | 自定义异常 |
com.eva.core.initializer | 项目初始化器 |
com.eva.core.model | 项目通用实体 |
com.eva.core.prevent | 防止操作,例如接口防重复的处理 |
com.eva.core.secure | 安全处理 |
com.eva.core.servlet | 请求处理 |
com.eva.core.session | 会话处理 |
com.eva.core.trace | 跟踪日志的处理 |
com.eva.core.utils | 通用工具类 |
设计思想
项目为四层结构,api > biz > service > dao,且biz和service直接使用class实现,无需定义接口。 现在让我们尝试着理解每一层的作用,为了更方便的理解,需要从后往前进行讲解!
- 持久层
持久层用于将 处理好的数据保存至数据库。这一层的代码可直接生成,不需要手动编写。
- 基础服务层
基础服务层用于 编写基础服务,例如怎样删除(逻辑删除还是物理删除),怎样批量删除(循环调用还是SQL实现)等。 在这一层中不涉及多表操作(除查询外)。这一层的代码可直接生成,不需要手动编写。
- 业务层
业务层用于 编写业务,这一层可能涉及到多表的增删改操作,而单表的增删改操作的业务实现在基础层。 所以业务层应该使用颗粒业务层对象,而不应该直接使用持久层对象。 当然,如果你的业务足够简单,例如删除一条单表记录,那么这一层可以不用实现,但这种做法并不推荐。
- 控制层/接口层
控制层/接口层用于 定义接口并调用相关业务方法,而业务方法可以来自业务层和基础服务层(严格来说,应来自业务层)。
# 服务端口配置
端口配置在application-config.yml
的app.port
属性中。调整后需重启服务。
# 接口文档
如果端口没有修改的话,那么接口地址为http://localhost:10010/doc.html (opens new window) 。但通常测试接口需要通过某个账号登录后才能调用, 此时可以访问接口文档认证页,完成认证后即可调用需要登录后才可调用的接口。认证页地址为http://localhost:10010/doc/auth.html (opens new window)。
关闭接口文档
您可以通过配置application-config.yml
文件内容来关闭接口文档,如下:
# 应用配置
app:
# 接口文档配置
api-doc:
# 是否启用,生产环境建议关闭
enabled: false
2
3
4
5
6
# 测试模式
某些时候我们为了测试方便,诸如短信、图形、密码等我们暂时不做验证以提高测试效率。为了方便的实现这一点,Eva提供了测试模式,只需要在application-config.yml
文件中配置运行模式即可。如下
# 应用配置
app:
...
# 模式,testing测试模式
mode: testing
2
3
4
5
当前测试模式开启后,不会校验图片验证码。如果需要获取是否为测试模式,可以使用以下代码
// 如果为测试模式
if (Utils.AppConfig.isTestingMode()) {
// TODO,执行测试模式下处理逻辑
}
2
3
4
# 权限控制
Eva4支持多个权限框架,为了使不同权限框架的用法保持一致,Eva4直接调用权限框架底层方法封装了自定义注解。
# @ContainRoles
@ContainRoles
注解用于配置接口要求用户拥有某(些)角色才可访问,它只有1个参数:
- value: 角色列表
示例1: 以下代码表示必须拥有admin
角色才可访问
@ContainRoles("admin")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2: 以下代码表示必须拥有admin
和 manager
角色才可访问
@ContainRoles({"admin", "manager"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# @ContainAnyRoles
@ContainAnyRoles
注解用于配置接口要求用户拥有某些角色之一才可访问,它只有1个参数:
- value: 角色列表
示例1: 以下代码表示必须拥有admin
或 manager
角色才可访问
@ContainAnyRoles({"admin", "manager"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# @ContainPermissions
@ContainPermissions
注解用于配置接口要求用户拥有某(些)权限才可访问,它只有1个参数:
- value: 权限列表
示例1: 以下代码表示必须拥有system:user:create
权限才可访问
@ContainPermissions("system:user:create")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2: 以下代码表示必须拥有system:user:create
和 system:user:update
权限才可访问
@ContainPermissions({"system:user:create", "system:user:update"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# @ContainAnyPermissions
@ContainAnyPermissions
注解用于配置接口要求用户拥有某些权限之一才可访问,它只有1个参数:
- value: 权限列表
示例1: 以下代码表示必须拥有system:user:create
或 system:user:update
权限才可访问
@ContainAnyPermissions({"system:user:create", "system:user:update"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# @AuthorizeExpress
@AuthorizeExpress
注解用于配置接口的权限表达式,它只有1个参数:
- value: 权限表达式
权限表达式用于表达权限关系,可以使用&&
和||
来表达并且和或者关系,结合以下表达式来处理较为复杂的权限关系。
- isSuperAdmin():是否为超级管理员
- hasRoles():是否包含某(些)角色
- hasAnyRoles():是否包含某(些)角色之一
- hasPermissions():是否包含某(些)权限
- hasAnyPermissions():是否包含某(些)权限之一
示例1: 以下代码表示必须为超级管理员才可访问
@AuthorizeExpress("isSuperAdmin()")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2: 以下代码表示必须为超级管理员或者拥有system:user:create
权限才可访问
@AuthorizeExpress("isSuperAdmin() || hasPermissions('system:user:create')")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
&&和||的优先级
表达式率先会根据||
分割成子表达式,子表达式再根据&&
分割成最小颗粒的表达式,||
两侧的任一表达式成立时权限验证才算通过。例如
A || B && C
,则表达式A
成立 或者 表达式B && C
成立时才算验证通过。
# 同时使用类和方法的权限控制
version >= 4.4.0
当类和方法上都添加权限注解时,表示必需同时满足。例如
@ContainRoles("normal")
public class DemoContrller extends BaseController {
@ContainPermissions("system:user:create")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
}
2
3
4
5
6
7
8
表示必需拥有normal
角色,并且还需要拥有system:user:create
权限才可访问create方法。
# 超级管理员的权限认证
version >= 4.4.0
超级管理员所拥有的权限分为两种情况
- 默认情况下,Eva没有给超级管理员角色配置任何权限,这表示超级管理员拥有所有权限。
- 若手动给超级管理员配置权限,则表示超级管理员仅包含所指定的权限。
# 无认证接口实现
有些时候我们允许不登录直接访问接口,此时可以直接在application-config.yml
中进行配置。如下
# 应用配置
app:
...
# 会话配置
session:
...
# 会话拦截
interceptor:
...
# 不需要拦截的路径(不需要登录即可访问)
exclude-path-patterns:
# 增加一些不需要进行会话拦截的路径
- /exclude/mypath
- /exclude/path/**
2
3
4
5
6
7
8
9
10
11
12
13
14
注意
系统中提供的需要认证才能访问的接口,请勿在此放行。这样接口获取不到用户信息导致出现错误。
# 事务处理
在需要进行事务处理的方法上添加@Transactional
注解即可。无论是service层还是biz层这种方式都是有效的。
误区
有些同学可能因为"懒"的缘故,直接在Controller中实现biz层做的事情,无论Controller方法上添加@Transactional
是否能使事务生效,这种做法都是不规范的。
# 分页实现
虽然分页代码可以直接生成,但我们仍然需要为您说明分页的实现逻辑。
# 单表的分页实现
当表的Service类继承了BaseService,BaseService提供了findPage方法,可直接实现分页,以下是一个示例
public PageData<MyModel> findPage(PageWrap<QueryMyModelDTO> pageWrap) {
MyModel model = new MyModel();
// 将查询参数复制到实体类中
BeanUtils.copyProperties(pageWrap.getModel(), model);
return myModelService.findPage(pageWrap.getPage(), pageWrap.getCapacity(), model);
}
2
3
4
5
6
其中MyModel
为实体类,QueryMyModelDTO
为查询条件参数类(该类也可以直接使用实体类)。findPage有多个重载方法,如下
service.findPage(int pageIndex, int capacity, Model model)
service.findPage(int pageIndex, int capacity, Model model, boolean withDeleted)
service.findPage(int pageIndex, int capacity, Model model, QueryWrapper<Model> queryWrapper)
参数 | 说明 |
---|---|
pageIndex | 页码 |
capacity | 每页数量 |
withDeleted | 是否包含已删除的数据 |
model | 查询条件 |
queryWrapper | 查询条件的封装 |
# 多表的分页实现
多表的分页需要编写SQL语句,GoldPanKit提供的多表接口插件可直接生成多表的SQL语句。但您仍然可以自行编写语句,假定编写完以后得到mapper.search
方法,则Biz层可参考以下代码实现分页:
@Override
public PageData<MyListVO> findPage(PageWrap<MyQueryDTO> pageWrap) {
// 使用MyBatisPlus提供的方法开启分页
PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
// 通过Utils.MP.blankToNull方法将查询参数中的空字符串转为null,避免SQL中缺少非空串判断
Utils.MP.blankToNull(pageWrap.getModel());
// 调用SQL语句查询出结果
List<MyListVO> myList = myMapper.search(pageWrap.getModel());
// 转为PageData对象
return PageData.from(new PageInfo<>(myList));
}
2
3
4
5
6
7
8
9
10
11
PageData和PageWrap
细心的同学可能已经发现,无论是单表分页还是多表分页,方法参数均为PageWrap
,方法返回均为PageData
,为什么不使用MyBatis
Plus自带的IPage?有经验的同学可能不难理解这是一种包装,这样可以方便的扩展我们想要的字段,并且无论分页方式如何变化,方法的请求参数和响应结构都能始终保持一致。
# 分页字段排序的实现
有时候我们需要实现列表中存在按自定义列排序的需求。Eva做了些简单的封装支持了字段排序的功能。
# 单表的分页字段排序
单表的分页字段排序可以使用MyBatis Plus默认的实现来处理。
public PageData<MyModel> findPage(PageWrap<QueryMyModelDTO> pageWrap) {
// 将查询参数复制到实体类中
MyModel model = new MyModel();
BeanUtils.copyProperties(pageWrap.getModel(), model);
// 通过Utils.MP.blankToNull方法将查询参数中的空字符串转为null,避免将空串作为查询条件
Utils.MP.blankToNull(model);
QueryWrapper<MyModel> queryWrapper = new QueryWrapper<>(model);
// 查询分页时将使用QueryWrapper作为findPage参数,这里需要手动添加仅查询未删除的数据条件
queryWrapper.lambda()
.eq(MyModel::getDeleted, false);
// 增加字段排序
for(PageWrap.SortData sortData: pageWrap.getSorts()) {
if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
queryWrapper.orderByDesc(sortData.getProperty());
} else {
queryWrapper.orderByAsc(sortData.getProperty());
}
}
return myModelService.findPage(pageWrap.getPage(), pageWrap.getCapacity(), queryWrapper);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 多表的分页字段排序
多表的排序需要结合新的方法参数和SQL处理来完成,不过实现也非常简单。
MyModelMapper.java
增加orderByClause,并通过@Param
注解标记SQL参数名称。
public interface MyMapper extends BaseMapper<MyModel> {
List<MyModelListVO> search(@Param("dto") QueryMyModelDTO dto, @Param("orderByClause") String orderByClause);
}
2
3
4
MyModelMapper.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="...">
<!-- 结果集映射 -->
<resultMap
id="MyModelListVO"
type="...MyModelListVO"
autoMapping="true"
>
<id column="id" property="id"/>
</resultMap>
<!-- 搜索方法 -->
<select id="search"
parameterType="...QueryMyModelDTO"
resultMap="MyModelListVO"
...>
SELECT ...
FROM ...
LEFT JOIN ...
LEFT JOIN ...
<where>
<if dto.param
!= null>
AND ...
</if>
</where>
${orderByClause}
</select>
</mapper>
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
MyModelBiz.java
@Override
public PageData<MyModelListVO> findPage(PageWrap<QueryMyModelDTO> pageWrap) {
// 使用MyBatisPlus提供的方法开启分页
PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
// 通过Utils.MP.blankToNull方法将查询参数中的空字符串转为null,避免SQL中缺少非空串判断
Utils.MP.blankToNull(pageWrap.getModel());
// 调用SQL语句查询出结果,给定排序参数
List<MyListVO> myList = myMapper.search(pageWrap.getModel(), pageWrap.getOrderByClause());
// 转为PageData对象
return PageData.from(new PageInfo<>(myList));
}
2
3
4
5
6
7
8
9
10
11
注意
MyModelMapper.java中使用@Param注解标识了SQL参数的名称,条件参数对象被标识为dto
,因此在使用条件字段时应使用dto.prop
(prop为字段名称),而不应直接使用prop
。
# 系统自带的通用接口
接口 | 说明 | 参数 |
---|---|---|
POST /oss/upload/image | 上传图片 | file :图片文件 |
POST /oss/upload/attach | 上传文件 | file :文件 |
GET /resource/oss/image | 访问图片 | f :图片文件的fileKey |
GET /resource/oss/attach | 下载文件 | f :文件上传后的fileKey, fn:下载后的文件名称 |
GET /resource/local/download | 下载本地文件 | path :文件路径 |
# 下载本地文件说明
下载本地文件接口用于处理一些固定的文件需要下载的场景,例如从Excel中导入数据时需要一份Excel文件模版!此时可以将文件存储在项目files目录下,并调用GET /resource/local/download
接口来下载文件。
如果要修改本地文件的存储路径,可直接修改application-config.yml
文件,如下
# 应用配置
app:
# 本地文件存储路径,"~"表示当前项目路径或部署路径
local-file-directory: ~/files
2
3
4
# 导入Excel
# 功能说明
导入Excel指的是将Excel中的数据经过系统导入到数据库中。
# 实现步骤
# 1. 制作Excel模版
在做Excel导入之前,应该先提供一个Excel模版用于下载,这样可以避免用户使用错误的Excel格式。模版制作后,可以查看系统自带的通用接口 中的下载本地文件接口来了解如何存放。
# 2. 配置Excel列信息
@Data
public class MyExcelImportDTO {
@ExcelImportColumn(name="姓名")
private String name;
@ExcelImportColumn(name="手机号码")
private String mobile;
}
2
3
4
5
6
7
8
9
# 3. 执行导入
当Controller获取到上传的Excel文件file后,可使用以下代码进行解析和导入。
ExcelImporter.build(MyExcelImportDTO.class).importData(file.getInputStream(), (rows, index) -> {
for (MyExcelImportDTO row : rows) {
// 创建或更新数据
}
}, sync);
2
3
4
5
importData方法参数说明
参数 | 说明 |
---|---|
InputStream is | 上传的Excel文件流 |
ExcelImportCallback callback | 导入数据的回调方法,参数为导入数据行和导入数据行的索引,返回值为导入数据的成功数。 |
boolean sync | 如果数据已存在,是否更新 |
# @ExcelImportColumn
@ExcelImportColumn
注解用于指定Excel导入时,绑定字段和Excel中的列。
参数 | 说明 |
---|---|
index | 列索引,指定字段读取Excel中的第几列,从0开始计数 |
name | 列名,指定字段在Excel中的列名 |
converter | 数据转换器 |
args | 数据转换器参数 |
# 数据转换
很多时候,Excel中的数据并不是我们真正想要得到的数据,而转换器的目的就是将Excel中的数据转为我们真正想要获取的数据。Eva默认提供了两个数据转换器。即DoubleToStringConverter
和IntegerToStringConverter
。分别用于将Double类型转为String和将Integer类型转为String。使用转换器非常的简单,如下:
public class MyExcelImportDTO {
@ExcelImportColumn(name="手机号码", converter=IntegerToStringConverter.class)
private String mobile;
}
2
3
4
5
# 自定义数据转换器
为了保证数据转换的可扩展性,Eva支持自定义的数据转换器,实现起来也非常简单,如下
public class MyDataConverter implements ExcelDataConverterAdapter {
@Override
Object format (Object value, String[] args, Cell cell, Workbook workbook) {
// value:单元格数据
// args:ExcelImportColumn或ExcelExportColumn注解的args参数
// cell:单元格对象
// workbook 工作簿对象
}
}
2
3
4
5
6
7
8
9
10
# 导出Excel
# 功能说明
导出Excel指的是将一个列表导出成一个Excel文件,方便其它部门查看或进一步处理。
# 实现步骤
在Eva中实现一个导出功能是非常简单的,如下
# 1. 配置Excel列信息
public class MyExportExcelVO {
@ExcelExportColumn(name="列名1")
private String name;
@ExcelExportColumn(name="列名2")
private String name2;
}
2
3
4
5
6
7
8
# 2. 实现导出接口
@Api(tags = "Xxx模块")
@RestController
@RequestMapping("/xx/xxx")
public class MyController extends BaseController {
@Resource
private MyService myService;
@ApiOperation("导出Excel")
@PostMapping("/export")
@RequiresPermissions("xx:xxx:xxxx")
public void exportExcel (HttpServletResponse response) {
ExcelExporter.build(MyExportExcelVO.class).exportData(myService.find(), "Excel文件名称", response);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# @ExcelExportColumn
@ExcelExportColumn注解用于导出Excel时,绑定字段和Excel中的列。
参数 | 说明 |
---|---|
name | 列名 |
width | 列宽 |
index | 列宽(单位为字符),-1按字段反射顺序排序 |
align | 对齐方式 |
backgroundColor | 列头背景色 |
dataBackgroundColor | 数据单元格的背景色 |
color | 字体颜色 |
fontSize | 字体大小(像素) |
bold | 是否加粗 |
italic | 是否倾斜 |
valueMapping | 值映射,如0=女;1=男 |
prefix | 数据前缀 |
suffix | 数据后缀 |
dateFormat | 日期格式,只有数据为java.util.Date时才生效 |
dict | 字典编码,指定后将自动根据编码获取字典数据,并找到与之匹配的数据标签 |
converter | 数据转换器 |
args | 数据转换器参数 |
authorize | 权限表达式,只有满足权限表达式时才会存在该列 |
# 数据转换
有时候我们希望数据展现为一个特殊的格式,或者需要对数据进一步的加工。此时我们可以通过数据转换器来实现,如下
public class MyExportExcelVO {
@ExcelExportColumn(name="列名1", converter=MyConverter.class)
private String name;
}
2
3
4
5
6
# 自定义数据转换器
与后端手册/导入Excel/自定义数据转换器保持一致。
# 防重复提交
在接口方法上添加@PreventRepeat
注解即可,参数如下
参数 | 说明 |
---|---|
handler | 防重复规则设定类,默认为PreventRepeatDefaultHandler.class |
interval | 间隔时间(ms),在此时间再次被调用视为重复调用,默认为800 |
message | 默认为“请求过于频繁” |
示例1:采用默认参数
@PreventRepeat
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2:指定防重复时间和错误消息
@PreventRepeat(interval = 1000, message = "请求过于频繁,请稍后再试!")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# 自定义防重复提交规则
默认情况下防重复的规则由PreventRepeatDefaultHandler.class
设定,它将请求路径、用户令牌、客户端IP信息合并在一起并签名作为请求的Key。如果你并不希望这样,那么你可以自定义防重复的规则。实现方式如下
@Component
public class MyPreventRepeatHandler extends PreventRepeatHandlerAdapter {
@Override
public String sign(HttpServletRequest request) {
// 根据您要验证的参数进行签名并返回即可
}
}
2
3
4
5
6
7
8
# 操作日志
# 智能模式和手动模式
Eva的操作日志分为两种模式——智能模式和手动模式,默认情况下使用的是智能模式。您可以在application-config.yml
文件中修改配置,如下
# 应用配置
app:
# 跟踪日志
trace:
# 开启智能跟踪模式
smart: true
# 排除跟踪的URL正则
exclude-patterns:
- .+/list[a-zA-Z0-9\-\_]*$
- .+/tree[a-zA-Z0-9\-\_]*$
- .+/page[a-zA-Z0-9\-\_]*$
- .+/all[a-zA-Z0-9\-\_]*$
- /swagger-resources.*
- /v2/api-docs.*
2
3
4
5
6
7
8
9
10
11
12
13
14
- 如果您想手动记录操作日志,可修改
smart
为false
,此时您需要通过@Trace
注解来标记出需要记录日志的接口。 - 智能模式下,如果接口不需要记录日志,可以在
exclude-patterns
配置项中增加接口路径或正则。也可以直接使用@Trace(exclude=true)
注解来排除。
# @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时表示日志中不记录请求结果 |
智能跟踪的原理和约定
智能模式自动识别的原理是通过正则匹配接口路径,为了能够准确的识别,你应该按如下约定进行接口路径的定义:
- 新增: 满足正则表达式
.+/create.*
- 修改: 满足正则表达式
.+/update.**
- 删除: 满足正则表达式
.+/delete.*
- 批量删除: 满足正则表达式
.+/delete/batch$
- 导入: 满足正则表达式
.+/import.*
- 导出: 满足正则表达式
.+/export.*
- 重置: 满足正则表达式
.+/reset.**
# 线程池
有时候我们需要做一些简单的异步处理,也就是说需要开启一个新的线程,而线程池的加入可以让线程更加的安全和可控。在Eva中,你可以使用以下代码来直接通过线程池开启一个新的线程。
Utils.ThreadPool.start(() -> {
// 线程代码
});
2
3
# 获取字典数据和系统配置
# 获取字典
为了方便获取字典数据,在系统启动时会将字典和字典数据一次性加载到缓存中,在缓存中的数据结构如下:
DictCache.java —— 字典缓存类
属性 | 说明 |
---|---|
Integer id | 字典ID |
String code | 字典编码 |
String name | 字典名称 |
List<DictDataCache> dataList | 字典数据列表 |
DictDataCache.java —— 字典数据缓存类
属性 | 说明 |
---|---|
Integer id | 字典数据ID |
String value | 字典数据值 |
String label | 字典数据标签 |
String config | 字典数据配置 |
因为字典数据已经被一次性加载,所以可以提供全局方法来直接获取,如下:
// 根据字典编码获取字典对象
DictCache dict = Utils.Dict.getDict("字典编码");
// 根据字典编码和数据值获取字典数据对象
DictDataCache dictData = Utils.Dict.getDictData("字典编码", "数据编码");
// 根据字典编码和数据值获取数据标签
String dataLabel = Utils.Dict.getDictDataLabel("字典编码", "数据编码");
2
3
4
5
6
7
8
# 获取系统配置
系统配置和字典一样,在启动项目时会一次性加载到缓存中,可通过全局方法直接获取
// 根据配置编码获取配置值
String config = Utils.AppConfig.get("配置编码");
2
# 安全通讯
对于安全性要求较高的项目,我们希望请求参数和响应参数都进行加密处理。这在Eva中实现起来非常简单,只需要配置加密接口即可,如下
application-config.yml
# 应用配置
app:
...
# 安全配置
security:
# 数据传输安全配置
transmission:
# 需要进行加密的接口路径
path-patterns:
- /system/login
# 不需要进行加密的接口路径
exclude-path-patterns:
# 密钥,用于请求和响应时传输参数的加解密,该密钥定义需要定义在前端,容易暴露,需和数据库密钥进行区分
key: 2B7E151628AED2A6
key-len: 128
iv: 3D8A9F0BAC4E7D61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
加密后Swagger如何调试?
Swagger可以正常明文传参,因为在Eva中,如果是Swagger发起的请求,则不会对请求参数进行解密,也不会加密响应内容。
# 2FA二次认证
某些敏感操作可能需要用户进行二次密码确认,例如退款审核、手动充值等。此时则可以利用2FA二次认证来完成。在Eva中实现2FA认证也是非常简单的,只需要在接口上添加@EnableTwoFA
注解即可,如下
@EnableTwoFA
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# 字段加密和脱敏处理
字段加密和脱敏指的是对敏感信息加密后存储,查询时解密后返回。如用户的手机号码、身份证号码均为敏感信息。同样可以通过注解来实现这一功能,如下
接口添加@EnableSecureField
注解启用安全字段
@EnableSecureField
public ApiResponse<?> create(@RequestBody CreateMyModelDTO dto) {
myModelBiz.create(dto);
return ApiResponse.success(null);
}
2
3
4
5
DTO/VO类添加@EnableSecureField
注解
@EnableSecureField
public class CreateMyModelDTO {
}
2
3
为DTO/VO类需要加密或解密的字段添加@SecureField
注解
@EnableSecureField
public class CreateMyModelDTO {
@SecureField
private String mobile;
}
2
3
4
5
6
通过以上步骤即可完成字段加密和脱敏处理。
为什么DTO/VO类还需要添加`@EnableSecureField`注解?
在DTO/VO类上添加@EnableSecureField
注解是为了提高参数解析的速度。因为接口参数没有定值,可能出现非常复杂的结构,此时如果对结构进行全面扫描是无用功。
通过注解标记出“安全字段所在类”,可以显著提高解析速度,快速发现安全字段。
# 实用工具类
所有工具类都通过Utils调用,这样你可以不用去查找工具类就可以方便的获取工具类对象。
# Utils.AppConfig/获取应用配置
配置在application-config.yml
中的配置均在AppConfig
类中定义了与之匹配的属性,可通过Utils.AppConfig
对象来获取,如下
// 获取超级管理员角色编码
String env = Utils.AppConfig.getSuperAdminRole();
// OSS文件访问路径前缀
String accessPrefix = Utils.AppConfig.getOSS().getAccessPrefix();
2
3
4
5
也可以获取当前配置是否为测试模式,详见测试模式 。也可直接获取系统配置,详见系统配置。
# Utils.OSS/文件上传和下载
Utils.OSS为OSS工具类,用于处理文件的上传与下载。它有如下方法:
/**
* 上传图片
*
* @param imageFile 图片文件
* @param businessPath 业务路径,如使用"/avatar"表示用户头像路径,"/goods/cover"表示商品封面图片等
* @return 文件访问路径
*/
public UploadResult uploadImage(MultipartFile imageFile, String businessPath);
/**
* 上传图片
*
* @param imageFile 图片文件
* @param businessPath 业务路径,如使用"/avatar"表示用户头像路径,"/goods/cover"表示商品封面图片等
* @return 文件访问路径
*/
public UploadResult uploadImage(String bucket, MultipartFile imageFile, String businessPath);
/**
* 上传文件
*
* @param file 文件
* @return 文件访问路径
*/
public UploadResult upload(MultipartFile file);
/**
* 上传文件
*
* @param file 文件
* @param businessPath 业务路径,如使用"/contract"表示合同文件,"/contract/attach"表示合同附件
* @return UploadResult
*/
public UploadResult upload(MultipartFile file, String businessPath);
/**
* 上传文件
*
* @param bucket 存储空间名称
* @param file 文件
* @param businessPath 业务路径,如使用"/contract"表示合同文件,"/contract/attach"表示合同附件
* @return UploadResult
*/
public UploadResult upload(String bucket, MultipartFile file, String businessPath);
/**
* 下载
*
* @param fileKey 文件在存储空间中的key
* @return InputStream
*/
public InputStream download (String fileKey);
/**
* 下载
*
* @param bucket 存储空间名称
* @param fileKey 文件在存储空间中的key
* @return InputStream
*/
public InputStream download(String bucket, String fileKey);
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
OSS.UploadResult属性说明
属性名称 | 类型 | 说明 |
---|---|---|
originalFilename | String | 源文件名称 |
fileKey | String | 文件的key |
accessUri | String | 访问路径/下载路径 |
示例1:文件大小和格式的限制
// 上传文件"avatar.jpg"
Utils.OSS
// 限定文件大小不超过5M
.setMaxSize(5)
// 限定只允许.png和.jpg格式文件的上传
.setFileTypes('.png,.jpg')
.uploadImage(file);
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "xxxxx.jpg", accessUri: "/resource/oss/image?f=xxxxx.jpg" }
2
3
4
5
6
7
8
示例2:指定业务路径/存储路径
// 上传文件"avatar.jpg"
Utils.OSS.uploadImage(file, "/avatar");
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "avatar/xxxxx.jpg", accessUri: "/avatar?f=xxxxx.jpg" }
2
3
业务路径的用途
您可能会遇见在下载/预览文件时需要对文件特殊处理的场景,如下载课程视频时添加水印,且文件存储在一个指定的bucket目录,则可以使用业务路径来实现:
// 上传课程视频
@PostMapping("/cource/video/upload")
public ApiResponse<OSSUtil.UploadResult> uploadVideo(MultipartFile file) {
return ApiResponse.success(Utils.OSS.upload(file, "/cource/video"));
}
// 下载课程视频
@GetMapping("/cource/video")
public void downloadVideo(@RequestParam(name = "f") String fileKey) {
// 注意去掉前方的“/”
InputStream is = Utils.OSS.download("cource/video/" + fileKey);
// 添加水印
// TODO
// 写出文件流到客户端
response.getOutputStream().write(...);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Utils.Dict/读取字典和字典数据
详见获取字典。
# Utils.Session/会话工具
/**
* 获取当前登录用户
*
* @return 当前登录用户信息
*/
public LoginUserInfo getLoginUser ();
2
3
4
5
6
# Utils.SpringContext/Spring上下文工具
/**
* 获取Bean实例
*
* @param name 类注册名称
* @return Object
*/
public Object getBean(String name);
/**
* 获取Bean实例
*
* @param clazz Class
* @return T
*/
public <T> T getBean(Class<T> clazz);
/**
* 获取Bean实例
*
* @param name 类注册名称
* @param clazz Class
* @return T
*/
public <T> T getBean(String name, Class<T> clazz);
/**
* 获取环境对象
*
* @return Environment
*/
public Environment getEnvironment();
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
# Utils.Location/地区工具
Utils.Location为地区工具类,它有如下方法:
/**
* 获取地区信息
*
* @param ip IP
* @return Info
*/
public Info getLocation (String ip);
/**
* 获取IP详细地址
*
* @param ip IP
* @return String
*/
public String getLocationString (String ip);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Utils.Http/请求工具
在Eva中,发起一次请求的基本逻辑如下:
- 构造HttpUtil.HttpWrap对象
- 请求设置(如设置请求头,请求超时时间等)
- 发起请求
- 请求结果转换
构造HttpUtil.HttpWrap对象
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
build | String url | HttpUtil.HttpWrap | 根据请求地址构造 |
build | String url, String charset | HttpUtil.HttpWrap | 根据请求地址和编码构造 |
请求设置
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
setRequestProperty | String key, String value | HttpUtil.HttpWrap | 设置请求头 |
setConnectTimeout | int timeout | HttpUtil.HttpWrap | 设置连接超时时间 |
setReadTimeout | int timeout | HttpUtil.HttpWrap | 设置读取超时时间 |
gzip | HttpUtil.HttpWrap | 开启gzip压缩 |
发起请求
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
get | HttpUtil.HttpResult | 发起GET请求 | |
post | HttpUtil.HttpResult | 发起POST请求 | |
post | String params | HttpUtil.HttpResult | 发起POST请求 |
postJSON | Map<String, Object> paramsMap | HttpUtil.HttpResult | 发起POST请求 |
postJSON | JSONObject paramJSONObject | HttpUtil.HttpResult | 发起POST请求 |
请求结果转换
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
toStringResult | String | 转为字符串 | |
toJSONObject | JSONObject | 转为fastjson JSONObject对象 | |
toClass | Class | 转为指定class后的对象 | 转为指定类对象 |
示例1: 发起GET请求
Utils.Http.build(url)
.get()
.toStringResult();
2
3
示例2: 发起POST请求
Utils.Http.build(url)
.post(data)
.toStringResult();
2
3
示例3: 开启gzip压缩
Utils.Http.build(url)
.gzip()
.post(data)
.toStringResult();
2
3
4
示例4: 设置请求编码
Utils.Http.build(url, Charset.forName("GBK").toString())
.get()
.toStringResult();
2
3
# Utils.Digest/获取文本摘要
/**
* 获取手机号码摘要
* 逻辑:前3个数字 + **** + 后4个数字
*
* @param mobile 手机号码
* @return 手机号摘要
*/
public String digestMobile (String mobile);
/**
* 获取邮箱摘要
* 逻辑:获取前缀,前缀第一个字符 + **** + 前缀最后一个字符 + 邮箱后缀
*
* @param email 邮箱
* @return 邮箱摘要
*/
public String digestEmail (String email);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 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.MP/MyBatis Plus工具
目前该工具仅提供了一个public <T> void blankToNull(T object)
方法,用于在做查询时,将查询参数对象中的空字符串转为null,避免将空串作为条件。
WHY?
为了防止MyBatis Plus中的blank to null与实际中的blank to null存在差异,因此,单独编写了该方法仅用于对查询条件对象的blank to null处理。
# Utils.Secure/安全工具
安全工具中有诸多关于安全层面的方法,下面仅列出常用的方法,更多方法请参考源码!
/**
* 加密字段
*
* @param plainText 明文
* @return 加密后的字段值
*/
public String encryptField(String plainText);
/**
* 解密字段
*
* @param cipherText 密文
* @return 解密后的字段值
*/
public String decryptField(String cipherText);
/**
* 加密密码,公式为:MD5(MD5(password) + salt)
*
* @param password 密码
* @param salt 密码盐
* @return String
*/
public String encryptPassword(String password, String salt);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Utils.Date/日期工具
/**
* 获取最近N天前的日期
*
* @return N天前的日期
*/
public Date getBeforeDay (int recentDay);
/**
* 获取昨天
* @return 昨天
*/
public Date getYesterday();
/**
* 获取当月的开始时间
*
* @return 当月开始时间
*/
public Date getMonthStart ();
/**
* 获取当月的结束时间
*
* @return 当月结束时间
*/
public Date getMonthEnd ();
/**
* 获取当年开始时间
*
* @return 当年开始时间
*/
public Date getYearStart ();
/**
* 获取当年结束时间
*
* @return 当年结束时间
*/
public Date getYearEnd ();
/**
* 获取日期的开始时间
*
* @param date 日期
* @return Date
*/
public Date getStart (Date date);
/**
* 获取日期的结束时间
*
* @param date 日期
* @return Date
*/
public Date getEnd (Date date);
/**
* 格式化
*
* @param date 日期
* @return String
*/
public String format (Date date);
/**
* 格式化
*
* @param date 日期
* @param format 格式
* @return String
*/
public String format (Date date, String format);
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Utils.ThreadPool/线程池
详见线程池
# Utils.AES/AES加密工具
/**
* 加密
*
* @param plainText 明文
* @param key 密钥
* @param keyLen 密钥长度
* @param iv 向量
* @return String
*/
public String encrypt(String plainText, String key, int keyLen, String iv);
/**
* 解密
*
* @param cipherText 密文
* @param key 密钥
* @param keyLen 密钥长度
* @param iv 向量
* @return String
*/
public String decrypt(String cipherText, String key, int keyLen, String iv);
/**
* 加密数据
*
* @return String
*/
public String encryptData(String plainText) throws SecurityException;
/**
* 解密传输参数
*
* @param cipherText 密文
* @return String
*/
public String decryptData(String cipherText) throws SecurityException;
/**
* 加密传输参数
*
* @return String
*/
public String encryptTransmission(String plainText) throws SecurityException;
/**
* 解密数据
*
* @param cipherText 密文
* @return String
*/
public String decryptTransmission(String cipherText) throws SecurityException;
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
44
45
46
47
48
49
50
51
# 响应状态定义及规范
系统的响应状态定义在ResponseStatus
枚举中,如下
@Getter
@AllArgsConstructor
public enum ResponseStatus {
// 400开头表示参数错误
BAD_REQUEST(4000, "参数错误"),
DATA_EMPTY(4001, "找不到数据,该数据可能已被删除"),
DATA_EXISTS(4002, "记录已存在"),
DATA_ERROR(4003, "数据错误"),
PWD_INCORRECT(4004, "密码不正确"),
VERIFICATION_CODE_INCORRECT(4005, "验证码不正确或已过期"),
ACCOUNT_INCORRECT(4006, "账号或密码不正确"),
LOCAL_FILE_NOT_EXISTS(4007, "文件不存在"),
PRIVILEGE_ERROR(4008, "疑似存在非法权限提升或不是最新数据,请刷新数据后重试"),
TWO_FA_INCORRECT(4009, "登录密码不正确"),
TWO_FA_REQUIRED(4010, "需要进行2FA认证"),
TWO_FA_FAILED(4011, "2FA认证失败"),
// 500开头表示未知的服务异常
SERVER_ERROR(5000, "系统繁忙,请联系系统管理员"),
EXPORT_EXCEL_ERROR(5010, "导出Excel失败,请联系系统管理员"),
IMPORT_EXCEL_ERROR(5011, "导入Excel失败,请联系系统管理员"),
// 510开头表示可能导致数据错误的异常
REPEAT_REQUEST(5100, "请勿重复提交"),
MASSIVE_REQUEST(5101, "请求过于频繁"),
NOT_ALLOWED(5110, "不允许的操作"),
;
private final int code;
private final String message;
}
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
响应码如何定义对程序不会产生直接影响,但我们希望您在使用Eva时可以培养一个好的习惯——定义可识别的响应码。除了目前定义的响应码外,下面再给出一些例子供参考
响应码前缀 | 说明 |
---|---|
10 | 以10开头表示用户模块错误,如1000表示登录过于频繁 |
11 | 以11开头表示会员模块错误,如1100表示会员已到期 |
12 | 以12开头表示订单模块错误,如1200表示货源不足 |
400 | 以400开头表示参数错误所导致的各类情况的响应码 |
500 | 以500开头表示程序因异常终止,不同类型的异常对应不同的响应码 |
510 | 以510开头表示请求可能导致数据错误 |
您可以按照您团队和业务的考量将响应码以其它方式规范化。这样的一个显而易见的好处是,我们仅凭响应码就能识别错误的严重性。
# 自定义全局异常处理
全局异常处理在类GlobalExceptionAdvice
中实现,通过spring的@RestControllerAdvice
注解实现。如果您需要全局捕获某异常,可以直接在GlobalExceptionAdvice
类中继续添加,默认情况下添加了以下异常:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionAdvice {
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public Object handleBusinessException (BusinessException e);
/**
* 无权限异常处理
*/
@ExceptionHandler(UnauthorizedException.class)
public Object handleUnauthorizedException (UnauthorizedException e);
/**
* 参数验证未通过异常处理
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Object handleMissingServletRequestParameterException (MissingServletRequestParameterException e);
/**
* 其它异常处理
*/
@ExceptionHandler(Exception.class)
public Object handleException (Exception e);
}
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
无法捕获异常?
如果您的异常无法捕获,您可以从以下几个方面着手检查
- 异常是否已被处理,即抛出异常后被catch,打印了日志或抛出了其它异常
- 异常是否非Controller抛出,即在拦截器或过滤器中出现的异常
# 业务异常处理
Eva为业务异常封装了异常对象BusinessException
,提供了若干个构造方法以方便构建,定义如下:
@Data
@EqualsAndHashCode(callSuper = false)
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(Integer code, String message);
public BusinessException(Integer code, String message, Throwable e);
public BusinessException(ResponseStatus status);
public BusinessException(ResponseStatus status, String message);
public BusinessException(ResponseStatus status, Throwable e);
public BusinessException(ResponseStatus status, String message, Throwable e);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 会话模式及其配置
Eva提供两种会话模式,分别是:
- INTERACTIVE:交互模式(默认),每次访问系统接口,都会自动延长会话过期时间至指定的会话过期时间。
- FIXED:固定模式,无论在会话过期时间内是否有访问系统,会话都会在指定的过期时间后失效。
您可以在application-config.yml
中配置,如下
# 应用配置
app:
# 会话配置
session:
# 会话模式
mode: INTERACTIVE
2
3
4
5
6
# 验证码过期时长
默认情况下,系统中自带了图片验证码,您可以在application-config.yml
配置图片验证码的过期时长,如下:
# 应用配置
app:
# 验证码
captcha:
# 图片验证码
image:
# 过期时长(s)
expire: 300
2
3
4
5
6
7
8