SpringBootWeb案例
SpringBootWeb案例
今日目标
- 员工管理
- 新增员工 ✏️
- 文件上传(本地上传和oss上传) ✏️
- 事务管理
- 修改员工 ✏️
- 新增员工 ✏️
知识准备
- 能理解controller,service,mapper三个包的含义以及调用顺序
- 能完成新增,修改,删除,查询基本操作
- 能理解前后端交互是用过JSON格式传递数据的
- 能说出HTTP协议的特点,特别是请求和响应的组成部分
员工增删改查
1. 员工模块 ✏️
1.1 新增员工
新增员工
新增员工
前面我们已经实现了员工信息的条件分页查询。 接下来我们要实现的是新增员工的功能实现,页面原型如下:

首先我们先完成"新增员工"的功能开发,而在新增员工中,需要添加头像后期讲解,而头像需要用到文件上传技术后期讲解。 当整个员工管理功能全部开发完成之后,我们再通过配置文件来优化一些内容。

在添加员工信息时,录入的信息包括两个部分,一个部分是员工的基本信息; 另一个部分是员工的工作经历信息,那这两部分信息最终在录入完成后,点击 "保存" 按钮后都会提交到服务器端。
最终,员工的基本信息是要保存在员工表 emp
中的。 而 员工的工作经历信息,要保存在员工工作经历信息表 emp_expr
中的。
我们参照接口文档来开发新增员工功能
基本信息
- 请求路径:
/emps
- 请求方式:
POST
- 接口描述:
该接口用于添加员工的信息
- 请求路径:
请求参数
- 参数格式:
application/json
- 参数说明:
名称 类型 是否必须 备注 username string 必须 用户名 name string 必须 姓名 gender number 必须 性别, 说明: 1 男, 2 女 image string 非必须 图像 deptId number 非必须 部门id entryDate string 非必须 入职日期 job number 非必须 职位, 说明: 1 班主任,2 讲师, 3 学工主管, 4 教研主管, 5 咨询师 salary number 非必须 薪资 exprList object[] 非必须 工作经历列表 |- company string 非必须 所在公司 |- job string 非必须 职位 |- begin string 非必须 开始时间 |- end string 非必须 结束时间 - 请求数据样例:
- 参数格式:
{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entryDate": "2022-09-18",
"deptId": 1,
"phone": "18809091234",
"salary": 8000,
"exprList": [
{
"company": "百度科技股份有限公司",
"job": "java开发",
"begin": "2012-07-01",
"end": "2019-03-03"
},
{
"company": "阿里巴巴科技股份有限公司",
"job": "架构师",
"begin": "2019-03-15",
"end": "2023-03-01"
}
]
}
响应数据
- 参数格式:
application/json
- 参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据 - 响应数据样例:
- 参数格式:
{
"code":1,
"msg":"success",
"data":null
}
代码操作
思路分析
思路分析
新增员工的具体的流程:

接口文档规定:
- 请求路径:/emps
- 请求方式:POST
- 请求参数:Json格式数据
- 响应数据:Json格式数据
问题1:如何限定请求方式是POST?
@PostMapping
问题2:怎么在controller中接收json格式的请求参数?
@RequestBody //把前端传递的json数据填充到实体类中
准备工作
-- 员工工作经历表
create table emp_expr(
id int unsigned primary key auto_increment comment 'ID, 主键',
emp_id int unsigned null comment '员工ID', -- 关联的是emp员工表的ID
begin date null comment '开始时间',
end date null comment '结束时间',
company varchar(50) null comment '公司名称',
job varchar(50) null comment '职位'
) comment '工作经历';
🎯工作经历的EmpExpr实体类、Mapper接口和Mapper映射文件,以及Emp实体类的改造
准备的EmpExprMapper
接口及映射配置文件EmpExprMapper.xml
,并准备实体类接收前端传递的json格式的请求参数。
- EmpExprMapper接口
@Mapper
public interface EmpExprMapper {
}
- EmpExprMapper.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="cn.yangeit.mapper.EmpExprMapper">
</mapper>
3). 需要在 Emp
员工实体类中增加属性 exprList
来封装工作经历数据。 最终完整代码如下:
/**
* 工作经历
*/
@Data
public class EmpExpr {
private Integer id; //ID
private Integer empId; //员工ID
private LocalDate begin; //开始时间
private LocalDate end; //结束时间
private String company; //公司名称
private String job; //职位
}
@Data
public class Emp {
private Integer id; //ID,主键
private String username; //用户名
private String password; //密码
private String name; //姓名
private Integer gender; //性别, 1:男, 2:女
private String phone; //手机号
private Integer job; //职位, 1:班主任,2:讲师,3:学工主管,4:教研主管,5:咨询师
private Integer salary; //薪资
private String image; //头像
private LocalDate entryDate; //入职日期
private Integer deptId; //关联的部门ID
private LocalDateTime createTime; //创建时间
private LocalDateTime updateTime; //修改时间
//封装部门名称数
private String deptName; //部门名称
//封装员工工作经历信息
private List<EmpExpr> exprList;
}
保存员工基本信息
1. EmpController
在 EmpController
中增加save方法。
/**
* 添加员工
*/
@PostMapping
public Result save(@RequestBody Emp emp){
log.info("请求参数emp: {}", emp);
empService.save(emp);
return Result.success();
}
2. EmpService & EmpServiceImpl
在 EmpService
中增加 save 方法
/**
* 添加员工
* @param emp
*/
void save(Emp emp);
在 EmpServiceImpl
中增加save方法 , 实现接口中的save方法
@Override
public void save(Emp emp) {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
//3. 保存员工的工作经历信息 - 批量 (稍后完成)
}
3. EmpMapper
在 EmpMapper
中增加insert方法,新增员工的基本信息。
/**
* 新增员工数据
*/
@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("insert into emp(username, name, gender, phone, job, salary, image, entry_date, dept_id, create_time, update_time) " +
"values (#{username},#{name},#{gender},#{phone},#{job},#{salary},#{image},#{entryDate},#{deptId},#{createTime},#{updateTime})")
void insert(Emp emp);
主键返回:@Options(useGeneratedKeys = true, keyProperty = "id")
由于稍后,我们在保存工作经历信息的时候,需要记录是哪位员工的工作经历。 所以,保存完员工信息之后,是需要获取到员工的ID的,那这里就需要通过Mybatis中提供的主键返回功能来获取。
测试:

一个员工,是可以有多段工作经历的,所以在页面上将来用户录入员工信息时,可以自己根据需要添加多段工作经历。页面原型展示如下:

那如果员工只有一段工作经历,我们就需要往工作经历表中保存一条记录。 执行的SQL如下:

如果员工有两段工作经历,我们就需要往工作经历表中保存两条记录。执行的SQL如下:

如果员工有三段工作经历,我们就需要往工作经历表中保存三条记录。执行的SQL如下:

所以,这里最终我们需要执行的是批量插入数据的insert语句。
1). EmpServiceImpl
完善save方法中保存员工信息的逻辑。完整逻辑如下:
@Autowired
EmpExprMapper empExprMapper;
@Override
public void save(Emp emp) {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
2). EmpExprMapper
@Mapper
public interface EmpExprMapper {
/**
* 批量插入员工工作经历信息
*/
public void insertBatch(List<EmpExpr> exprList);
}
3). EmpExprMapper.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="cn.yangeit.mapper.EmpExprMapper">
<!--批量插入员工工作经历信息-->
<insert id="insertBatch">
insert into emp_expr (emp_id, begin, end, company, job) values
<foreach collection="exprList" item="expr" separator=",">
(#{expr.empId}, #{expr.begin}, #{expr.end}, #{expr.company}, #{expr.job})
</foreach>
</insert>
</mapper>
这里用到Mybatis中的动态SQL里提供的
<foreach>
标签,改标签的作用,是用来遍历循环,常见的属性说明:
- collection:集合名称
- item:集合遍历出来的元素/项
- separator:每一次遍历使用的分隔符
- open:遍历开始前拼接的片段
- close:遍历结束后拼接的片段
上述的属性,是可选的,并不是所有的都是必须的。 可以自己根据实际需求,来指定对应的属性。

功能测试
代码开发完成后,重启服务器,打开 Apifox 发送 POST 请求,请求路径:http://localhost:8080/emps

请求完毕后,可以打开idea的控制台看到控制台输出的日志:

前后端联调
功能测试通过后,我们再进行通过打开浏览器,测试后端功能接口:

点击保存之后,可以看到列表中已经展示出了这条数据。

总结
课堂作业
- 截止到现在,学习了哪些动态sql标签?分别又什么作用?🎤
2. 事务管理 🍐
2.1 事务介绍
事务管理
目前我们实现的新增员工功能中,操作了两次数据库,执行了两次 insert 操作。
- 第一次:保存员工的基本信息到
emp
表中。 - 第二次:保存员工的工作经历信息到
emp_expr
表中。
如果说,保存员工的基本信息成功了,而保存员工的工作经历信息出错了,会发生什么现象呢?那接下来,我们来做一个测试 。 我们可以在代码中,人为在保存员工的service层的save方法中,构造一个错误:

那接下来,我们就重启服务,打开浏览器,来做一个测试:

点击 “保存” 之后,提示 “系统接口异常”。

我们可以打开IDEA控制台看一下,报出的错误信息。 我们看到,保存了员工的基本信息之后,系统出现了异常。

我们再打开数据库,看看表结构中的数据是否正常。
1). emp
员工表中是有 Jerry
这条数据的。

2). emp_expr
表中没有改员工的工作经历信息。

最终,我们看到,程序出现了异常 ,员工表 emp
数据保存成功了, 但是 emp_expr
员工工作经历信息表,数据保存失败了。 那是否允许这种情况发生呢?
- 不允许
- 因为这属于一个业务操作,如果保存员工信息成功了,保存工作经历信息失败了,就会造成数据库数据的不完整、不一致。
那如何解决这个问题呢? 这需要通过数据库中的事务来解决这个问题。
概念: 事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。
就拿添加员工的这个业务为例,在这个业务操作中,包含了两个操作,那这两个操作是一个不可分割的工作单位。

这两个操作,要么同时失败,要么同时成功。
默认MySQL的事务是自动提交的 ,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务。
代码操作
事务控制主要三步操作:开启事务、提交事务/回滚事务。
- 需要在这组操作执行之前,先开启事务 (
start transaction; / begin;
)。 - 所有操作如果全部都执行成功,则提交事务 (
commit;
)。 - 如果这组操作中,有任何一个操作执行失败,都应该回滚事务 (
rollback
)。
那接下来,我们就可以将添加员工的业务操作,进行事务管理。 具体的SQL如下:
-- 开启事务
start transaction; -- 或者 begin;
-- 1. 保存员工基本信息
insert into emp values (39, 'Tom', '123456', '汤姆', 1, '13300001111', 1, 4000, '1.jpg', '2023-11-01', 1, now(), now());
-- 2. 保存员工的工作经历信息
insert into emp_expr(emp_id, begin, end, company, job) values (39,'2019-01-01', '2020-01-01', '百度', '开发'), (39,'2020-01-10', '2022-02-01', '阿里', '架构');
-- 提交事务(全部成功)
commit;
-- 回滚事务(有一个失败)
rollback;
事务管理的场景,是非常多的,比如:
- 银行转账
- 下单扣减库存
总结
课堂作业
- 事务有什么特点和作用?🎤
2.2 Spring事务管理 🍐 🍐
Spring事务管理
在上述实现的新增员工的功能中,一旦在保存员工基本信息后出现异常。 我们就会发现,员工信息保存成功,但是工作经历信息保存失败,造成了数据的不完整不一致。

产生原因:
- 先执行新增员工的操作,这步执行完毕,就已经往员工表
emp
插入了数据。 - 执行 1/0 操作,抛出异常
- 抛出异常之前,下面所有的代码都不会执行了,批量保存工作经历信息,这个操作也不会执行 。
此时就出现问题了,员工基本信息保存了,员工的工作经历信息未保存,业务操作前后数据不一致。
而要想保证操作前后,数据的一致性,就需要让新增员工中涉及到的两个业务操作,要么全部成功,要么全部失败 。
那我们如何,让这两个操作要么全部成功,要么全部失败呢 ?
那就可以通过事务 来实现,因为一个事务中的多个业务操作,要么全部成功,要么全部失败。
此时,我们就需要在新增员工功能中添加事务。

在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。
思考:开发中所有的业务操作,一旦我们要进行控制事务,是不是都是这样的套路?
答案:是的。
所以在spring框架当中就已经把事务控制的代码都已经封装好了,并不需要我们手动实现。我们使用了spring框架,我们只需要通过一个简单的注解@Transactional
就搞定了。
代码操作
@Transactional
作用:就是在当前这个方法执行开始之前来开启事务,方法执行完毕之后提交事务。如果在这个方法执行的过程当中出现了异常,就会进行事务的回滚操作。
@Transactional
注解:我们一般会在业务层当中来控制事务,因为在业务层当中,一个业务功能可能会包含多个数据访问的操作。在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
@Transactional注解书写位置:
- 方法
- 当前方法交给spring进行事务管理
- 类
- 当前类中所有的方法都交由spring进行事务管理 (推荐)
- 接口
- 接口下所有的实现类当中所有的方法都交给spring 进行事务管理
接下来,我们就可以在业务方法delete上加上@Transactional
来控制事务 。
@Transactional
@Override
public void save(Emp emp) {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
int i = 1/0;
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
说明:可以在application.yml
配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息了
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
在业务功能上添加@Transactional
注解进行事务管理后,我们重启SpringBoot服务,使用 Apifox测试:

添加Spring事务管理后,由于服务端程序引发了异常,所以事务进行回滚。

打开数据库,我们会看到 emp
表 与 emp_expr
表中都没有对应的数据信息,保证了数据的一致性、完整性。 🎯
总结一下:
- 在业务层类或者接口上、或者方法上,添加
@Transactional
注解进行事务管理 - 开启事务管理的日志,观察信息
事务进阶
前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional
注解当中的两个常见的属性:
- 异常回滚的属性:
rollbackFor
- 事务传播行为:
propagation
我们先来学习下rollbackFor属性。
rollbackFor
我们在之前编写的业务方法上添加了@Transactional
注解,来实现事务管理。
@Transactional
@Override
public void save(Emp emp) {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
int i = 1/0;
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
以上业务功能save方法在运行时,会引发除0的算术运算异常(运行时异常),出现异常之后,由于我们在方法上加了
@Transactional
注解进行事务管理,所以发生异常会执行rollback回滚操作,从而保证事务操作前后数据是一致的。
下面我们在做一个测试,我们修改业务功能代码,在模拟异常的位置上直接抛出Exception异常(编译时异常)
@Transactional
@Override
public void save(Emp emp) {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
//模拟:异常发生
if(true){
throw new Exception("出现异常了~~~");
}
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
说明:在service中向上抛出一个Exception编译时异常之后,由于是controller调用service,所以在controller中要有异常处理代码,此时我们选择在controller中继续把异常向上抛。
重新启动服务后,打开Apifox进行测试,请求添加员工的接口:

通过Apifox返回的结果,我们看到抛出异常了。然后我们在回到IDEA的控制台来看一下。

我们看到数据库的事务居然提交了,并没有进行回滚。
通过以上测试可以得出一个结论:默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
@Transactional(rollbackFor = Exception.class)
@Override
public void save(Emp emp) throws Exception {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
//int i = 1/0;
if(true){
throw new Exception("出异常啦....");
}
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
}
接下来我们重新启动服务,测试新增员工的操作:

控制台日志,可以看到因为出现了异常又进行了事务回滚

结论 :
- 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
- 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
propagation
我们接着继续学习@Transactional
注解当中的第二个属性propagation,这个属性是用来配置事务的传播行为 的。
什么是事务的传播行为呢?
- 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
例如:两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法。

所谓事务的传播行为,指的就是在A方法运行的时候,
- 首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,
- 还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。
要想控制事务的传播行为,在@Transactional
注解的后面指定一个属性propagation,通过 propagation 属性来指定传播行为。接下来我们就来介绍一下常见的事务传播行为。
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
总结
课堂作业
- 事务的传播行为,你了解哪些知识点?在什么场景下会使用到 REQUIRES_NEW 🎤
2.3 事物传播行为之案例
事物传播行为之案例
接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。
**需求:**在新增员工信息时,无论是成功还是失败,都要记录操作日志。
步骤:
- 准备日志表 emp_log、实体类EmpLog、Mapper接口EmpLogMapper
- 在新增员工时记录日志
代码操作
准备工作:
1). 创建数据库表 emp_log
日志表:
-- 创建员工日志表
create table emp_log(
id int unsigned primary key auto_increment comment 'ID, 主键',
operate_time datetime comment '操作时间',
info varchar(2000) comment '日志信息'
) comment '员工日志表';
2). 引入资料中提供的实体类:EmpLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class EmpLog {
private Integer id; //ID
private LocalDateTime operateTime; //操作时间
private String info; //详细信息
}
3). 引入资料中提供的Mapper接口:EmpLogMapper
@Mapper
public interface EmpLogMapper {
//插入日志
@Insert("insert into emp_log (operate_time, info) values (#{operateTime}, #{info})")
public void insert(EmpLog empLog);
}
4). 引入资料中提供的业务接口:EmpLogService
public interface EmpLogService {
//记录新增员工日志
public void insertLog(EmpLog empLog);
}
5). 引入资料中提供的业务实现类:EmpLogServiceImpl
@Service
public class EmpLogServiceImpl implements EmpLogService {
@Autowired
private EmpLogMapper empLogMapper;
@Transactional
@Override
public void insertLog(EmpLog empLog) {
empLogMapper.insert(empLog);
}
}
业务实现类:EmpServiceImpl
@Autowired
private EmpMapper empMapper;
@Autowired
private EmpExprMapper empExprMapper;
@Autowired
private EmpLogService empLogService;
@Transactional(rollbackFor = {Exception.class})
@Override
public void save(Emp emp) {
try {
//1.补全基础属性
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
//2.保存员工基本信息
empMapper.insert(emp);
int i = 1/0;
//3. 保存员工的工作经历信息 - 批量
Integer empId = emp.getId();
List<EmpExpr> exprList = emp.getExprList();
if(!CollectionUtils.isEmpty(exprList)){
exprList.forEach(empExpr -> empExpr.setEmpId(empId));
empExprMapper.insertBatch(exprList);
}
} finally {
//记录操作日志
EmpLog empLog = new EmpLog(null, LocalDateTime.now(), emp.toString());
empLogService.insertLog(empLog);
}
}
测试:
重新启动SpringBoot服务,测试新增员工操作 。我们可以看到控制台中输出的日志:

- 执行了插入员工数据的操作
- 执行了插入日志操作
- 程序发生Exception异常
- 执行事务回滚(保存员工数据、插入操作日志 因为在一个事务范围内,两个操作都会被回滚)
然后在 emp_log
表中没有记录日志数据

原因分析:
接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。
在执行
save
方法时开启了一个事务当执行
empLogService.insertLog
操作时,insertLog
设置的事务传播行是默认值REQUIRED,表示有事务就加入,没有则新建事务此时:
save
和insertLog
操作使用了同一个事务,同一个事务中的多个操作,要么同时成功,要么同时失败,所以当异常发生时进行事务回滚,就会回滚save
和insertLog
操作
解决方案:
在EmpLogServiceImpl
类中insertLog方法上,添加 @Transactional(propagation = Propagation.REQUIRES_NEW)
Propagation.REQUIRES_NEW :不论是否有事务,都创建新事务 ,运行在一个独立的事务中。
@Service
public class EmpLogServiceImpl implements EmpLogService {
@Autowired
private EmpLogMapper empLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void insertLog(EmpLog empLog) {
empLogMapper.insert(empLog);
}
}
重启SpringBoot服务,再次测试 新增员工的操作 ,会看到具体的日志如下:

那此时,EmpServiceImpl
中的 save
方法运行时,会开启一个事务。 当调用 empLogService.insertLog(empLog)
时,也会创建一个新的事务,那此时,当 insertLog
方法运行完毕之后,事务就已经提交了。 即使外部的事务出现异常,内部已经提交的事务,也不会回滚了,因为是两个独立的事务。
到此事务传播行为已演示完成,事务的传播行为我们只需要掌握两个:REQUIRED、REQUIRES_NEW。
REQUIRED :大部分情况下都是用该传播行为即可。
REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
总结
课堂作业
- 结合上述步骤,完成代码,体会事务传播行为的应用场景!!🎤
2.4 事务四大特性 🍐
事务四大特性
面试题:事务有哪些特性?
- 原子性(Atomicity):事务是不可分割的最小单元,要么全部成功,要么全部失败。
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
事务的四大特性简称为:ACID
原子性(Atomicity) :原子性是指事务包装的一组sql是一个不可分割的工作单元,事务中的操作要么全部成功,要么全部失败。
一致性(Consistency):一个事务完成之后数据都必须处于一致性状态。
- 如果事务成功的完成,那么数据库的所有变化将生效。
- 如果事务执行出现错误,那么数据库的所有变化将会被回滚(撤销),返回到原始状态。
隔离性(Isolation):多个用户并发的访问数据库时,一个用户的事务不能被其他用户的事务干扰,多个并发的事务之间要相互隔离。
- 一个事务的成功或者失败对于其他的事务是没有影响。
持久性(Durability):一个事务一旦被提交或回滚,它对数据库的改变将是永久性的,哪怕数据库发生异常,重启之后数据亦然存在。
3. 文件上传 🚩 ✏️ 🍐
3.1 本地文件上传-4
文件上传
- 新增员工功能中,还存在一个问题:没有头像(图片缺失),怎么解决?

使用文件上传,将图片存储到本地或者阿里云服务器中
文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。
文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

在我们的案例中,在新增员工的时候,要上传员工的头像,此时就会涉及到文件上传的功能。在进行文件上传时,我们点击加号或者是点击图片,就可以选择手机或者是电脑本地的图片文件了。当我们选择了某一个图片文件之后,这个文件就会上传到服务器,从而完成文件上传的操作。
想要完成文件上传这个功能需要涉及到两个部分:
- 前端程序
- 服务端程序
1.在前端程序中要完成代码: 前端代码规则,了解一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
上传文件的原始form表单,要求表单必须具备以下三点(上传文件页面三要素):
1️⃣ 表单必须有file域,用于选择要上传的文件
<input type="file" name="image"/>
2️⃣ 表单提交方式必须为POST
通常上传的文件会比较大,所以需要使用 POST 提交方式
3️⃣ 表单的编码类型enctype必须要设置为:multipart/form-data
普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data
在提供的"课程资料"中有一个名叫"文件上传"的文件夹,直接将里的"upload.html"文件,复制到springboot项目工程下的static目录里面。

2. 后端程序接受前端上传文件步鄹:
首先在服务端定义这么一个controller,用来进行文件上传,然后在controller当中定义一个方法来处理
/upload
请求在定义的方法中接收提交过来的数据 (方法中的形参名和请求参数的名字保持一致)
- 用户名:String name
- 年龄: Integer age
- 文件: MultipartFile image
Spring中提供了一个API:MultipartFile,使用这个API就可以来接收到上传的文件

如果表单项的名字和方法中形参名不一致,使用@RequestParam注解进行参数绑定
需求1:下面我们来验证:删除form表单中enctype属性值,会是什么情况?
- 在IDEA中直接使用浏览器打开upload.html页面

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
- 选择要上传的本地文件

- 点击"提交"按钮,进入到开发者模式观察


我们再来验证:设置form表单中enctype属性值为multipart/form-data
,会是什么情况?
<form action="/upload" method="post" enctype="multipart/form-data">
姓名: <input type="text" name="username"><br>
年龄: <input type="text" name="age"><br>
头像: <input type="file" name="image"><br>
<input type="submit" value="提交">
</form>


接下来,创建后端ploadController,接收前端传过来的图片
UploadController代码:
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) {
log.info("文件上传:{},{},{}",username,age,image);
return Result.success();
}
}
测试
后端程序编写完成之后,打个断点,以debug方式启动SpringBoot项目

打开浏览器输入:http://localhost:8080/upload.html , 录入数据并提交

通过后端程序控制台可以看到,上传的文件是存放在一个临时目录

打开临时目录可以看到以下内容:

表单提交的三项数据(姓名、年龄、文件),分别存储在不同的临时文件中:

当我们程序运行完毕之后,这个临时文件会自动删除。
所以,我们如果想要实现文件上传,需要将这个临时文件,要转存到我们的磁盘目录中。
本地存储
前面我们已分析了文件上传功能前端和后端的基础代码实现,文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上持久化。
代码实现:
- 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
- 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下
MultipartFile 常见方法:
- String getOriginalFilename(); //获取原始文件名
- void transferTo(File dest); //将接收的文件转存到磁盘文件中
- long getSize(); //获取文件的大小,单位:字节
- byte[] getBytes(); //获取文件内容的字节数组
- InputStream getInputStream(); //获取接收到的文件内容的输入流
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+originalFilename));
return Result.success();
}
}
利用postman/apifox测试:
注意:请求参数名和controller方法形参名保持一致



通过postman测试,我们发现文件上传是没有问题的。但是由于我们是使用原始文件名作为所上传文件的存储名字,当我们再次上传一个名为1.jpg文件时,发现会把之前已经上传成功的文件覆盖掉。 👇 👇 👇
解决方案:保证每次上传文件时文件名都唯一的(使用UUID获取随机文件名)
@Slf4j
@RestController
public class UploadController {
@PostMapping("/upload")
public Result upload(String username, Integer age, MultipartFile image) throws IOException {
log.info("文件上传:{},{},{}",username,age,image);
//获取原始文件名
String originalFilename = image.getOriginalFilename();
//构建新的文件名
String extname = originalFilename.substring(originalFilename.lastIndexOf("."));//文件扩展名
String newFileName = UUID.randomUUID().toString()+extname;//随机名+文件扩展名
//将文件存储在服务器的磁盘目录
image.transferTo(new File("E:/images/"+newFileName));
return Result.success();
}
}
在解决了文件名唯一性的问题后,我们再次上传一个较大的文件(超出1M)时发现,后端程序报错:

报错原因呢是因为:在SpringBoot中,文件上传时默认单个文件最大大小为1M
那么如果需要上传大文件,可以在application.yml进行如下配置:
spring:
#在spring的下级
servlet:
multipart:
max-file-size: 10MB #配置单个文件最大上传大小
max-request-size: 100MB #配置单个请求最大上传大小(一次请求可以上传多个文件)
到时此,我们文件上传的本地存储方式已完成了。
本地存储方式的不足

如果直接存储在服务器的磁盘目录中,存在以下缺点:
- 不安全:磁盘如果损坏,所有的文件就会丢失
- 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
- 无法直接访问
通常有两种解决方案:
- 自己搭建存储服务器,如:fastDFS 、MinIO 实用
- 使用现成的云服务,如:阿里云,腾讯云,华为云简单
总结
课堂作业
- 前端上传文件必须满足哪3个要素
- 为什么要使用UUID算法给图片重新取名字?🎤
- 本地存储的缺点有哪些?🎤
- 结合UUID练习一下本地存储
3.2 阿里云OSS
阿里云OSS
阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商 。

云服务指的就是通过互联网对外提供的各种各样的服务,比如像:语音服务、短信服务、邮件服务、视频直播服务、文字识别服务、对象存储服务等等。
当我们在项目开发时需要用到某个或某些服务,就不需要自己来开发了,可以直接使用阿里云提供好的这些现成服务就可以了。比如:在项目开发当中,我们要实现一个短信发送的功能,如果我们项目组自己实现,将会非常繁琐,因为你需要和各个运营商进行对接。而此时阿里云完成了和三大运营商对接,并对外提供了一个短信服务。我们项目组只需要调用阿里云提供的短信服务,就可以很方便的来发送短信了。这样就降低了我们项目的开发难度,同时也提高了项目的开发效率。(大白话:别人帮我们实现好了功能,我们只要调用即可)
云服务提供商给我们提供的软件服务通常是需要收取一部分费用的。、
阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。

在我们使用了阿里云OSS对象存储服务之后,我们的项目当中如果涉及到文件上传这样的业务,在前端进行文件上传并请求到服务端时,在服务器本地磁盘当中就不需要再来存储文件了。我们直接将接收到的文件上传到oss,由 oss帮我们存储和管理,同时阿里云的oss存储服务还保障了我们所存储内容的安全可靠。

那我们学习使用这类云服务,我们主要学习什么呢?其实我们主要学习的是如何在项目当中来使用云服务完成具体的业务功能。而无论使用什么样的云服务,阿里云也好,腾讯云、华为云也罢,在使用第三方的服务时,操作的思路都是一样的。

SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
简单说,sdk中包含了我们使用第三方云服务时所需要的依赖,以及一些示例代码。我们可以参照sdk所提供的示例代码就可以完成入门程序。
阿里云oss对象存储使用步骤
第三方服务使用的通用思路,我们做一个简单介绍之后,接下来我们就来介绍一下我们当前要使用的阿里云oss对象存储服务具体的使用步骤。

Bucket:存储空间是用户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
下面我们根据之前介绍的使用步骤,完成准备工作:
- 注册阿里云账户(注册完成后需要实名认证)
- 注册完账号之后,就可以登录阿里云

- 通过控制台找到对象存储OSS服务

如果是第一次访问,还需要开通对象存储服务OSS


- 开通OSS服务之后,就可以进入到阿里云对象存储的控制台

- 点击左侧的 "Bucket列表",创建一个Bucket


大家可以参照"资料\04. 阿里云oss"中提供的文档,开通阿里云OSS服务。点击查看阿里云文档
总结
课堂作业
- 阿里云对象存储OSS是什么?有什么用途?🎤
- Bucket是什么意思?有什么作用?🎤
- 开通阿里云账号,然后创建一个Bucket存储文件。✏️
3.3 阿里OSS入门和集成-5
OSS入门和集成
需求1:创建Bucket后,传入一张本地图片至OSS,然后通过返回的链接,直接访问图片
需求2:集成OSS到项目中
需求1:测试阿里云API
核心步骤
- 创建测试工程,引入依赖
- 准备测试类
- 获取AccessKeyId以及配置工具类AliOSSUtils
(1)创建测试工程,引入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
(2)从官网获取测试方法
代码中,需要替换的内容为:
- accessKeyId:阿里云账号AccessKey
- accessKeySecret:阿里云账号AccessKey对应的秘钥
- bucketName:Bucket名称
- objectName:对象名称,在Bucket中存储的对象的名称
- filePath:文件路径
import org.junit.jupiter.api.Test;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.FileInputStream;
import java.io.InputStream;
public class AliOssTest {
@Test
public void testOss(){
// Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
String accessKeyId = "---------------------";
String accessKeySecret = "-----------------------";
// 填写Bucket名称,例如examplebucket。
String bucketName = "-----------";
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = "0001.jpg";
// 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
String filePath= "C:\\Users\\Administrator\\Pictures\\Saved Pictures\\10.jpg";
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (Exception ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
}
需求2:集成OSS到项目中
阿里云oss对象存储服务的准备工作以及入门程序我们都已经完成了,接下来我们就需要在案例当中集成oss对象存储服务,来存储和管理案例中上传的图片。

在新增员工的时候,上传员工的图像,而之所以需要上传员工的图像,是因为将来我们需要在系统页面当中访问并展示员工的图像。而要想完成这个操作,需要做两件事:
- 需要上传员工的图像,并把图像保存起来(存储到阿里云OSS)
- 访问员工图像(通过图像在阿里云OSS的存储地址访问图像)
- OSS中的每一个文件都会分配一个访问的url,通过这个url就可以访问到存储在阿里云上的图片。所以需要把url返回给前端,这样前端就可以通过url获取到图像。
我们参照接口文档来开发文件上传功能: 👇 👇
基本信息
- 请求路径:
/upload
- 请求方式:
POST
- 接口描述:上传图片接口
- 请求路径:
请求参数
- 参数格式:
multipart/form-data
- 参数说明:
参数名称 参数类型 是否必须 示例 备注 image file 必须 - 参数格式:
响应数据
- 参数格式:
application/json
- 参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据,上传图片的访问路径 - 响应数据样例:
{ "code": 1, "msg": "success", "data": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-0400.jpg" }
- 参数格式:
集成OSS到项目中代码实现:
引入阿里云OSS上传文件工具类(由官方的示例代码改造而来) 👇 👇
com.xx.utils 工具包
@Component // 表明当前类需要被spring进行实例化,放到ioc容器中
public class AliOSSUtils {
private String endpoint = "https://oss-cn-shanghai.aliyuncs.com";
private String accessKeyId = "LTAI5t9MZK8iq5T2Av5GLDxX";
private String accessKeySecret = "C0IrHzKZGKqU8S7YQcevcotD3Zd5Tc";
private String bucketName = "web-framework01";
/**
* 实现上传图片到OSS
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 避免文件覆盖
String originalFilename = multipartFile.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
//上传文件到 OSS
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(bucketName, fileName, inputStream);
//文件访问路径
String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
// 关闭ossClient
ossClient.shutdown();
return url;// 把上传到oss的路径返回
}
}
修改UploadController代码: 👈 👈
@Slf4j
@RestController
public class UploadController {
@Autowired
private AliOSSUtils aliOSSUtils;
@PostMapping("/upload")
public Result upload(MultipartFile image) throws IOException {
//调用阿里云OSS工具类,将上传上来的文件存入阿里云
String url = aliOSSUtils.upload(image);
//将图片上传完成后的url返回,用于浏览器回显展示
return Result.success(url);
}
}
使用postman测试:

总结
课堂作业
- 🚩 注册阿里云账号,开通Oss服务,完成上述服务,并且能够使用清晰的描述出集成oss流程。(可以使用流程图工具进行绘制)
4. 员工模块
4.1 删除员工-6
删除员工

当我们勾选列表前面的复选框,然后点击 "批量删除" 按钮,就可以将这一批次的员工信息删除掉了。也可以只勾选一个复选框,仅删除一个员工信息。
问题:我们需要开发两个功能接口吗?一个删除单个员工,一个删除多个员工
答案:不需要。 只需要开发一个功能接口即可(删除多个员工包含只删除一个员工)
接口文档
删除员工
基本信息
- 请求路径:
/emps
- 请求方式:
DELETE
- 接口描述:该接口用于批量删除员工的数据信息
- 请求路径:
请求参数
- 参数格式:查询参数
- 参数说明:
参数名 类型 示例 是否必须 备注 ids 数组 array 1,2,3 必须 员工的id数组 - 请求参数样例:
/emps?ids=1,2,3
响应数据
- 参数格式:
application/json
- 参数说明:
参数名 类型 是否必须 备注 code number 必须 响应码,1 代表成功,0 代表失败 msg string 非必须 提示信息 data object 非必须 返回的数据 - 响应数据样例:
{ "code":1, "msg":"success", "data":null }
- 参数格式:
代码操作

Controller接收参数
在 EmpController
中增加如下方法 delete
,来执行批量删除员工的操作。
方式一:在Controller方法中通过数组来接收
多个参数,默认可以将其封装到一个数组中,需要保证前端传递的参数名 与 方法形参名称保持一致。
/**
* 批量删除员工
*/
@DeleteMapping
public Result delete(Integer[] ids){
log.info("批量删除部门: ids={} ", Arrays.asList(ids));
return Result.success();
}
方式二:在Controller方法中通过集合来接收
也可以将其封装到一个List<Integer>
集合中,如果要将其封装到一个集合中,需要在集合前面加上 @RequestParam
注解。
/**
* 批量删除员工
*/
@DeleteMapping
public Result delete(@RequestParam List<Integer> ids){
log.info("批量删除部门: ids={} ", ids);
empService.deleteByIds(ids);
return Result.success();
}
两种方式,选择其中一种就可以,我们一般推荐选择集合,因为基于集合操作其中的元素会更加方便。
Service
1). 在接口中 EmpService
中定义接口方法 deleteByIds
/**
* 批量删除员工
*/
void deleteByIds(List<Integer> ids);
2). 在实现类 EmpServiceImpl
中实现接口方法 deleteByIds
在删除员工信息时,既需要删除 emp 表中的员工基本信息,还需要删除 emp_expr 表中员工的工作经历信息
@Transactional
@Override
public void deleteByIds(List<Integer> ids) {
//1. 根据ID批量删除员工基本信息
empMapper.deleteByIds(ids);
//2. 根据员工的ID批量删除员工的工作经历信息
empExprMapper.deleteByEmpIds(ids);
}
由于删除员工信息,既要删除员工基本信息,又要删除工作经历信息,操作多次数据库的删除,所以需要进行事务控制。
Mapper
1). 在 EmpMapper
接口中增加 deleteByIds
方法实现批量删除员工基本信息
/**
* 批量删除员工信息
*/
void deleteByIds(List<Integer> ids);
2). 在 EmpMapper.xml
配置文件中, 配置对应的SQL语句
<!--批量删除员工信息-->
<delete id="deleteByIds">
delete from emp where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
3). 在 EmpExprMapper
接口中增加 deleteByEmpIds
方法实现根据员工ID批量删除员工的工作经历信息
/**
* 根据员工的ID批量删除工作经历信息
*/
void deleteByEmpIds(List<Integer> empIds);
4). 在 EmpExprMapper.xml
配置文件中, 配置对应的SQL语句
<!--根据员工的ID批量删除工作经历信息-->
<delete id="deleteByEmpIds">
delete from emp_expr where emp_id in
<foreach collection="empIds" item="empId" open="(" close=")" separator=",">
#{empId}
</foreach>
</delete>
功能测试
功能开发完成后,重启项目工程,打开 Apifox,发起DELETE请求:

控制台SQL语句:

前后端联调
打开浏览器,测试后端功能接口:
