基于SpringBoot实现SSMP整合的案例之九 (业务消息一致性与功能的扩展和维护)

46 阅读7分钟

业务消息一致性处理

目前的功能制作基本上达成了正常使用的情况,也就是这个程序不出BUG。若我们搞一个BUG出来,会发现程序马上崩溃掉。比如后台手工抛出一个异常,看看前端接收到的数据什么样子。

@PostMapping
public R save(@RequestBody Book book) throws IOException {
    //手动制作一个BUG
    if (true) throw new IOException();
    return new R(bookService.save(book));
}
{
    "timestamp": "2023-06-22T11:40:09.266+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/books"
}

面对这种情况,前端的同学又不会了,这又是什么格式?怎么和之前的格式不一样?

{
    "flag": true,
    "data":{
        "id": 1,
        "type": "计算机理论",
        "name": "Spring实战 第5版",
        "description": "Spring入门经典教程"
    }
}

注意:不仅要对正确的操作数据格式做处理,还要对错误的操作数据格式做同样的格式处理。

首先在当前的数据结果中添加消息字段,用来兼容后台出现的操作消息。

@Data
public class R{
    private Boolean flag;
    private Object data;
    private String msg;		//用于封装消息
    
    // 添加构造函数,处理当flag==false时的情况
    public R(Boolean flag,String msg){
        this.flag=flag;
        this.msg=msg;
    }
}

后台代码也要根据情况做处理,当前是模拟的错误。

@PostMapping
public R save(@RequestBody Book book) throws IOException {
    //手动制作一个BUG
    if (true) throw new IOException();
    return new R(bookService.save(book));
}

然后在表现层做统一的异常处理,使用SpringMVC提供的异常处理器做统一的异常处理。

// 作为SpringMVC的异常处理器
// @ControllerAdvice
@RestControllerAdvice
public class ProjectExceptionAdvice {
    //添加此注解后,方法就可以拦截所有的异常信息
    // @ExceptionHandler
    //也可以给注解添加参数,来标明处理具体的异常
    @ExceptionHandler(Exception.class)
    public R doException(Exception ex){
        //记录日志
        //发送消息给运维
        //发送邮件给开发人员,ex对象发送给开发人员
        ex.printStackTrace();
        return new R(false,"服务器异常,请稍后再试!");
    }
}

此时页面接收到的消息:

{
    "flag": false,
    "data": null,
    "msg": "服务器异常,请稍后再试!"
}

页面上得到数据后,先判定是否有后台传递过来的消息,标志就是当前操作是否成功,如果返回操作结果false,就读取后台传递的消息。

//添加
handleAdd () {
	//发送ajax请求
    axios.post("/books",this.formData).then((res)=>{
        //如果操作成功,关闭弹层,显示数据
        if(res.data.flag){
            this.dialogFormVisible = false;
            this.$message.success("添加成功");
        }else {
            //消息来自于后台传递过来,而非固定内容
            this.$message.error(res.data.msg);
        }
    }).finally(()=>{
        this.getAll();
    });
},

修改手动添加的BUG,不能定死。

@PostMapping
public R save(@RequestBody Book book) throws IOException {
    //当book的Name属性为123时,抛出异常
    if (book.getName().equals("123")) throw new IOException();
    return new R(bookService.save(book));
}

SpringBoot_SSMP_ex123.png

问题:当前的提示信息,部分由前端管理也有部分由后端管理,耦合了!现在来分析如何将消息都归于后端来管理。

对页面的添加功能做修改:

//添加
handleAdd () {
	axios.post("/books",this.formData).then((res)=>{
	//判断当前操作是否成功
	if(res.data.flag){
            //1.关闭弹层
            this.dialogFormVisible = false;
            this.$message.success(res.data.msg);
	}else{
             this.$message.error(res.data.msg);
         }
	}).finally(()=>{
		//2.重新加载数据
		this.getAll();
	});
},

在BookController.java中修改BUG区代码:

@PostMapping
public R save(@RequestBody Book book) throws IOException {
    Boolean flag = bookService.save(book);
    //手动制作一个BUG
    if (book.getName().equals("123")) throw new IOException();
    return new R(flag , flag ? "添加成功^_^" : "添加失败-_-!");
}

总结

  1. 使用注解@RestControllerAdvice定义SpringMVC异常处理器用来处理异常的
  2. 异常处理器必须被扫描加载,否则无法生效
  3. 表现层返回结果的模型类中添加消息属性用来传递消息到页面

页面功能开发

分页功能

分页功能的制作用于替换前面的查询全部,其中要使用到elementUI提供的分页组件。

<!--分页组件-->
<div class="pagination-container">
    <el-pagination
		class="pagiantion"
		//当修改页码时调用此操作
		@current-change="handleCurrentChange"
		:current-page="pagination.currentPage"
		:page-size="pagination.pageSize"
		//总页数前一页当前页码值后一页前往哪一页
		layout="total, prev, pager, next, jumper"
		:total="pagination.total">
    </el-pagination>
</div>

为了配合分页组件,封装分页对应的数据模型。

data:{
	pagination: {	
		//分页相关模型数据
		currentPage: 1,	//当前页码
		pageSize:10,	//每页显示的记录数
		total:0,	//总记录数
	}
},

修改查询全部功能为分页查询,通过路径变量传递页码信息参数。

getAll() {
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
    });
},

后台提供对应的分页功能。

@GetMapping("/{currentPage}/{pageSize}")
public R getAll(@PathVariable Integer currentPage,@PathVariable Integer pageSize){
    IPage<Book> pageBook = bookService.getPage(currentPage, pageSize);
    return new R(null != pageBook ,pageBook);
}

页面根据分页操作结果读取对应数据,并进行数据模型绑定。

getAll() {
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize).then((res) => {
        this.pagination.total = res.data.data.total;
        this.pagination.currentPage = res.data.data.current;
        this.pagination.pagesize = res.data.data.size;
        this.dataList = res.data.data.records;
    });
},

对切换页码操作设置调用当前分页操作。

//切换页码
handleCurrentChange(currentPage) {
    // 修改页码值为当前选中的页码值
    this.pagination.currentPage = currentPage;
    // 执行查询
    this.getAll();
},

由于使用了分页功能,当最后一页只有一条数据时,删除操作就会出现BUG,最后一页无数据但是独立展示,对分页查询功能进行后台功能维护,如果当前页码值大于最大页码值,重新执行查询。

对于BUG,原则上来说需要基于业务需求来维护删除功能,而不是有统一方案来解决一切BUG。

@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize){
    IPage<Book> pageBook = bookService.getPage(currentPage, pageSize);
    // 此处解决删完未退出当前页BUG
    // 如果当前页码值大于总页码值,使用最大页码值作为当前页码值
    if (currentPage > pageBook.getPages()){
        pageBook = bookService.getPage((int) pageBook.getPages(),pageSize);
    }
    return new R(true,pageBook);
}

总结

  1. 使用el分页组件
  2. 定义分页组件绑定的数据模型
  3. 异步调用获取分页数据
  4. 分页数据页面回显

条件查询功能

最后一个功能来做条件查询,其实条件查询可以理解为分页查询的时候除了携带分页数据再多带几个数据的查询。这些多带的数据就是查询条件。比较一下不带条件的分页查询与带条件的分页查询差别之处,这个功能就好做了

  • 页面封装的数据:带不带条件影响的仅仅是一次性传递到后台的数据总量,由传递2个分页相关数据转换成2个分页数据加若干个条件
  • 后台查询功能:查询时由不带条件,转换成带条件,反正不带条件的时候查询条件对象使用的是null,现在换成具体条件,差别不大
  • 查询结果:不管带不带条件,出来的数据只是有数量上的差别,其他都差别,这个可以忽略

经过上述分析,看来需要在页面发送请求的格式方面做一定的修改,后台的调用数据层操作时发送修改,其他没有区别。

页面发送请求时,两个分页数据仍然使用路径变量,其他条件采用动态拼装url参数的形式传递。

页面封装查询条件字段

pagination: {		
//分页相关模型数据
	currentPage: 1,		//当前页码
	pageSize:10,		//每页显示的记录数
	total:0,			//总记录数
	name: "",
	type: "",
	description: ""
},

页面添加查询条件字段对应的数据模型绑定名称

<div class="filter-container">
    <el-input placeholder="图书类别" v-model="pagination.type" class="filter-item"/>
    <el-input placeholder="图书名称" v-model="pagination.name" class="filter-item"/>
    <el-input placeholder="图书描述" v-model="pagination.description" class="filter-item"/>
    <el-button @click="getAll()" class="dalfBut">查询</el-button>
    <el-button type="primary" class="butT" @click="handleCreate()">新建</el-button>
</div>

将查询条件组织成url参数,添加到请求url地址中,这里可以借助其他类库快速开发,当前使用手工形式拼接,降低学习要求

getAll() {
    // 获取查询条件,拼接查询条件
    param = "?name="+this.pagination.name;
    param += "&type="+this.pagination.type;
    param += "&description="+this.pagination.description;
    console.log("-----------------"+ param);
    axios.get("/books/"+this.pagination.currentPage+"/"+this.pagination.pageSize+param).then((res) => {
        this.pagination.pageSize = res.data.data.size;
        this.pagination.currentPage = res.data.data.current;
        this.pagination.total = res.data.data.total;
        this.dataList = res.data.data.records;
    });
},

后台代码中定义实体类封查询条件,Controller接收参数

@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage,@PathVariable int pageSize,Book book){
    IPage<Book> pageBook = bookService.getPage(currentPage, pageSize, book);
    if (currentPage > pageBook.getPages()){
        pageBook = bookService.getPage((int) pageBook.getPages(),pageSize,book);
    }
    return new R(true,pageBook);
}

对应业务层接口与实现类进行修正

public interface BookService {
    IPage<Book> getPage(int currentPage, int pageSize, Book book);
}
@Service
public class BookServiceImpl implements BookService {
    @Override
    public IPage<Book> getPage(int currentPage, int pageSize, Book book) {
        IPage page = new Page(currentPage,pageSize);
        LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<>();
        lqw.like(Strings.isNotEmpty(book.getType()),Book::getType,book.getType());
        lqw.like(Strings.isNotEmpty(book.getName()),Book::getName,book.getName());
        	lqw.like(Strings.isNotEmpty(book.getDescription()),Book::getDescription,book.getDescription());
        bookDao.selectPage(page,lqw);
        return page;
    }
}

总结

  1. 定义查询条件数据模型(当前封装到分页数据模型中)
  2. 异步调用分页功能并通过请求参数传递数据到后台

至此,案例完结。