概述
- 增加电子书管理页面:页面,路由,菜单
- 电子书列表展示:表格组件,查询表单,后端列表查询接口
- 前后端分页处理:后端分页插件,前端分页组件,前后端分页参数传递
- 电子书编辑:模态框组件,表单组件,后端保存接口
- 电子书新增:界面复用编辑功能,后端复用保存接口,雪花算法
- 电子书删除:确认组件,后端删除接口
- 参数校验:集成SpringBoot Validation
增加电子书管理页面
1. 增加电子书页面
因为电子书管理(管理员)页面和电子书访问(用户)页面的访问群体不一样,所以我们在前端项目的src/view当中新建一个admin文件夹,下面全部放管理的页面。
我们在src/view/admin当中创建一个文件叫做:admin-ebook.vue,然后内容我们后面再补充。
2. 增加电子书菜单
然后在src/components/the-header.vue当中使用router-link组件配置跳转到电子书管理的按钮即可:
<a-menu-item key="/"><router-link to="/">首页</router-link></a-menu-item><a-menu-item key="/admin/ebook" :style="user.id? {} : {display:'none'}"><router-link to="/admin/ebook">电子书管理</router-link></a-menu-item><a-menu-item key="/about"><router-link to="/about">关于我们</router-link></a-menu-item>
3. 增加电子书路由
添加路由非常简单,我们在src/router/index.ts当中引入,然后配置即可:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'import AdminEbook from '../views/admin/admin-ebook.vue' // 1. 引入组件const routes: Array<RouteRecordRaw> = [// 2.配置路由{path: '/admin/ebook',name: 'AdminEbook',component: AdminEbook,},]const router = createRouter({history: createWebHistory(process.env.BASE_URL),routes})export default router
电子书表格展示
1. Ant Design Vue表格组件介绍
我们可以登录Ant Design Vue的官网进行学习,但是由于我们使用的是vue3,所以Ant Design Vue的版本必须是V2以上。
2. 增加电子书表格展示
我们直接来阅读admin-ebook.vue的代码内容:
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><a-table:columns="columns" // 表头:row-key="record => record.id" // 每行的id做为key值:data-source="ebooks" // 数据来源:pagination="pagination" // 分页:loading="loading" // 有载入的效果@change="handleTableChange"><template #cover="{ text: cover }"> // 1-2 对封面特殊展示成图片,cover就是这行的值,在数据库里是一个路径<img v-if="cover" :src="cover" alt="avatar" /></template><template v-slot:action="{ text, record }"> // 2-2 对操作特殊展示成为两个按钮<a-space size="small"><a-button type="primary" @click="edit(record)">编辑</a-button><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger">删除</a-button></a-popconfirm></a-space></template></a-table></a-layout-content></a-layout></template><script lang="ts">import { defineComponent, onMounted, ref } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';import {Tool} from "@/util/tool";export default defineComponent({name: 'AdminEbook',setup() {const ebooks = ref(); // 定义数据源const pagination = ref({ // 定义分页current: 1,pageSize: 10,total: 0});const loading = ref(false); // 定义载入状态// 表格表头const columns = [{title: '封面',dataIndex: 'cover',slots: { customRender: 'cover' } // 1-1 对封面做特殊展示},{title: '名称',dataIndex: 'name'},{title: '分类',slots: { customRender: 'category' }},{title: '文档数',dataIndex: 'docCount'},{title: '阅读数',dataIndex: 'viewCount'},{title: '点赞数',dataIndex: 'voteCount'},{title: 'Action',key: 'action',slots: { customRender: 'action' } // 2-1 对操作做特殊展示}];/*** 数据查询**/const handleQuery = (params: any) => {loading.value = true;// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据ebooks.value = [];axios.get("/ebook/list", {params: {page: params.page,size: params.size,name: param.value.name}}).then((response) => {loading.value = false;const data = response.data;if (data.success) {ebooks.value = data.content.list;// 重置分页按钮pagination.value.current = params.page;pagination.value.total = data.content.total;} else {message.error(data.message);}});};/*** 表格点击页码时触发*/const handleTableChange = (pagination: any) => {console.log("看看自带的分页参数都有啥:" + pagination);handleQuery({page: pagination.current,size: pagination.pageSize});};onMounted(() => {handleQueryCategory();});return {ebooks,pagination,columns,loading,handleTableChange,//handleQuery没有在html当中使用,所以不需要返回}}});</script><style scoped>img {width: 50px;height: 50px;}</style>
✿PageHelper实现后端分页
1. 集成PageHelper插件
先在pom.xml当中添加下面的依赖(可以直接上maven仓库上拷贝过来):
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.2.13</version></dependency>
下载好依赖之后可以直接使用,不需要添加任何配置,当然如果我们想看sql查询的日志和结果,我们可以在resources/application.properties当中最后添加下面这么一段配置:
# 打印所有sql日志,sql,参数和结果logging.level.com.taopoppy.wiki.mapper=trace
这一个配置就是将mapper里面的日志级别变为最低,意思就是所有都打印出来。然后我们说明一下这个分页的原理,就是最基本的话要查两次,一次是总数据,一次是当前页的列表数据。
2. 修改电子书列表接口
下面我们在EbookService当中使用PageHelper:
public List<EbookResp> list(EbookReq req) {EbookExample ebookExample = new EbookExample();EbookExample.Criteria criteria = ebookExample.createCriteria();if(!ObjectUtils.isEmpty(req.getName())) {criteria.andNameLike("%" + req.getName() + "%");}PageHelper.startPage(1, 3); // 在selectByExample前面使用PageHelperList<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);List<EbookResp> respList = CopyUtil.copyList(ebooksList, EbookResp.class);return respList;}
特别注意,我们最好是将PageHelper的代码写在ebookMapper.selectByExample前面,因为PageHelper只作用在后面代码的第一次查询当中,也就是说如果PageHelper和ebookMapper.selectByExample中间如果还有别的查询,比如用户查询等等,PageHelper就不会作用在ebookMapper.selectByExample了,而是作用在离他最近的一次查询。
3. PageInfo类
除了PageHelper之外,还提供了另一个类PageInfo,可以通过它来获取总行数,总页数等信息:
public class EbookService {// 这个就是用来打印的,在WikiApplication当中也用到了private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);@Resourceprivate EbookMapper ebookMapper;public List<EbookResp> list(EbookReq req) {EbookExample ebookExample = new EbookExample();EbookExample.Criteria criteria = ebookExample.createCriteria();if(!ObjectUtils.isEmpty(req.getName())) {criteria.andNameLike("%" + req.getName() + "%");}PageHelper.startPage(1, 3);List<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);PageInfo<Ebook> pageInfo = new PageInfo<>(ebooksList); // PageInfoLOG.info("总行数:{}",pageInfo.getTotal()); // 总行数LOG.info("总页数:{}",pageInfo.getPages()); // 总页数,一般不需要,前端会根据总数自动计算总页数List<EbookResp> respList = CopyUtil.copyList(ebooksList, EbookResp.class);return respList;}}
根据上面的代码我们基本就可以确切的获得分页的四个要素:
- 前端传入的两个参数:第几页(page),每页多少条数据(pageSize)
- 后端返回的两个参数:数据列表(respList),总函数(total)
✿封装分页的请求和返回参数
根据上面的分页的四个要素,我们就来封装请求参数(PageReq)和返回参数(PageResp):
1. 请求参数的封装
我们现在req文件夹当中创建PageReq:
package com.taopoppy.wiki.req;public class PageReq {private int page;private int size;public int getPage() {return page;}public void setPage(int page) {this.page = page;}public int getSize() {return size;}public void setSize(int size) {this.size = size;}@Overridepublic String toString() {final StringBuffer sb = new StringBuffer("PageReq{");sb.append("page=").append(page);sb.append(", size=").append(size);sb.append('}');return sb.toString();}}
但是我们现在进入到后端的是EbookReq,所以如果后端也要能收到分页信息,我们将EbookReq类去继承PageReq,其他页面类型需要分类的也可以去继承PageReq:
public class EbookReq extends PageReq // 继承PageReq
这个时候我们在EbookService和EbookController当中的代码都不需要大改动,只需要把我们前面获取page和size书写成为动态化即可,在EbookService当中:
public List<EbookResp> list(EbookReq req) {EbookExample ebookExample = new EbookExample();EbookExample.Criteria criteria = ebookExample.createCriteria();if(!ObjectUtils.isEmpty(req.getName())) {criteria.andNameLike("%" + req.getName() + "%");}// page和size都从req当中可以获取到PageHelper.startPage(req.getPage(), req.getSize());List<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);PageInfo<Ebook> pageInfo = new PageInfo<>(ebooksList);LOG.info("总行数:{}",pageInfo.getTotal());LOG.info("总页数:{}",pageInfo.getPages());return respList;}
2. 返回参数的封装
我们首先在resp文件夹下面创建一个PageResp.java,内容如下:
package com.taopoppy.wiki.resp;import java.util.List;public class PageResp<T> {private long total; // 返回总数private List<T> list; // 数据列表,需要泛型支撑public long getTotal() {return total;}public void setTotal(long total) {this.total = total;}public List<T> getList() {return list;}public void setList(List<T> list) {this.list = list;}@Overridepublic String toString() {final StringBuffer sb = new StringBuffer("PageResp{");sb.append("total=").append(total);sb.append(", list=").append(list);sb.append('}');return sb.toString();}}
然后我们修改EbookService.java当中的内容:
public PageResp<EbookResp> list(EbookReq req) {EbookExample ebookExample = new EbookExample();EbookExample.Criteria criteria = ebookExample.createCriteria();if(!ObjectUtils.isEmpty(req.getName())) {criteria.andNameLike("%" + req.getName() + "%");}PageHelper.startPage(req.getPage(), req.getSize());List<Ebook> ebooksList = ebookMapper.selectByExample(ebookExample);PageInfo<Ebook> pageInfo = new PageInfo<>(ebooksList);LOG.info("总行数:{}",pageInfo.getTotal());LOG.info("总页数:{}",pageInfo.getPages());List<EbookResp> respList = CopyUtil.copyList(ebooksList, EbookResp.class);// 把返回数据类型改成PageResp<EbookResp>PageResp<EbookResp> pageResp = new PageResp();pageResp.setTotal(pageInfo.getTotal());pageResp.setList(respList);return pageResp;}
改完之后我们还需要修改EbookController当中的部分代码:
@GetMapping("/list")public CommonResp list(EbookReq req) {// CommonResp<list<EbookResp>> 改为 CommonResp<PageResp<EbookResp>>CommonResp<PageResp<EbookResp>> resp = new CommonResp<>();PageResp<EbookResp> list = ebookService.list(req);resp.setContent(list);return resp;}
前后端分页功能整合
上面我们处理了后端分页的功能,现在我们来处理前端,以此达到前后端整合的效果,因为我们之前将管理员管理的页面也加入进来,所以对于电子书列表我们需要改动两个页面,一个是views/home.vue,另一个就是views/admin/admin-ebook.vue:
/*** 数据查询**/const handleQuery = (params: any) => {loading.value = true;// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据ebooks.value = [];axios.get("/ebook/list", {params: {page: params.page,size: params.size,name: param.value.name}}).then((response) => {loading.value = false;const data = response.data;if (data.success) {ebooks.value = data.content.list;// 重置分页按钮pagination.value.current = params.page;pagination.value.total = data.content.total;} else {message.error(data.message);}});};onMounted(() => {handleQuery();});
/*** 查询数据*/const handleQueryEbook = () => {axios.get("/ebook/list", {params: {page: 1,size: 1000,categoryId2: categoryId2}}).then((response) => {const data = response.data;ebooks.value = data.content.list;// ebooks1.books = data.content;});};
制作电子书表单
本小节的功能就是点击每一行的编辑按钮,弹出编辑框,然后编辑框显示电子书表单,对应的就是ant design vue当中的一个model框而已,这个在官网看一下就会了
1. 前端代码展示
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><p><a-form layout="inline" :model="param"><a-form-item><a-input v-model:value="param.name" placeholder="名称"></a-input></a-form-item><a-form-item><a-button type="primary" @click="handleQuery({page: 1, size: pagination.pageSize})">查询</a-button></a-form-item><a-form-item><a-button type="primary" @click="add()">新增</a-button></a-form-item></a-form></p><a-table:columns="columns":row-key="record => record.id":data-source="ebooks":pagination="pagination":loading="loading"@change="handleTableChange"><template #cover="{ text: cover }"><img v-if="cover" :src="cover" alt="avatar" /></template><template v-slot:category="{ text, record }"><span>{{ getCategoryName(record.category1Id) }} / {{ getCategoryName(record.category2Id) }}</span></template><template v-slot:action="{ text, record }"><a-space size="small"><router-link :to="'/admin/doc?ebookId=' + record.id"><a-button type="primary">文档管理</a-button></router-link><a-button type="primary" @click="edit(record)">编辑</a-button><a-popconfirmtitle="删除后不可恢复,确认删除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger">删除</a-button></a-popconfirm></a-space></template></a-table></a-layout-content></a-layout><a-modaltitle="电子书表单"v-model:visible="modalVisible":confirm-loading="modalLoading"@ok="handleModalOk"><a-form :model="ebook" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"><a-form-item label="封面"><a-input v-model:value="ebook.cover" /></a-form-item><a-form-item label="名称"><a-input v-model:value="ebook.name" /></a-form-item><a-form-item label="分类"><a-cascaderv-model:value="categoryIds":field-names="{ label: 'name', value: 'id', children: 'children' }":options="level1"/></a-form-item><a-form-item label="描述"><a-input v-model:value="ebook.description" type="textarea" /></a-form-item></a-form></a-modal></template><script lang="ts">import { defineComponent, onMounted, ref } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';import {Tool} from "@/util/tool";export default defineComponent({name: 'AdminEbook',setup() {// -------- 表单 ---------/*** 数组,[100, 101]对应:前端开发 / Vue*/const categoryIds = ref();const ebook = ref();const modalVisible = ref(false);const modalLoading = ref(false);const handleModalOk = () => {modalLoading.value = true;ebook.value.category1Id = categoryIds.value[0];ebook.value.category2Id = categoryIds.value[1];axios.post("/ebook/save", ebook.value).then((response) => {modalLoading.value = false;const data = response.data; // data = commonRespif (data.success) {modalVisible.value = false;// 重新加载列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};/*** 编辑*/const edit = (record: any) => {modalVisible.value = true;ebook.value = Tool.copy(record);categoryIds.value = [ebook.value.category1Id, ebook.value.category2Id]};}});</script>
上面的代码非常简单,只不过我们在点击编辑弹出表单后,我们修改完毕提交表单,要请求一个ebook的保存接口,接口我们后面来在spring boot当中添加。
2. 后端新增修改接口
实际上保存的接口,我们不需要返回什么,返回成功的标志信息就可以,在CommonResp类当中的content不需要放值:
@GetMapping("/list")public CommonResp list(EbookQueryReq req) {// controller层尽量不要出现Ebook这个实体类// 因为实体类是和数据库一一对应的CommonResp<PageResp<EbookResp>> resp = new CommonResp<>();PageResp<EbookResp> list = ebookService.list(req);resp.setContent(list);return resp;}@PostMapping("/save")public CommonResp save(@RequestBody EbookSaveReq req) {CommonResp resp = new CommonResp<>();ebookService.save(req);return resp;}
可以看到我们为了区分两个接口要传进来的参数类,我们分为了EbookQueryReq和EbookSaveReq两个类,前者是之前EbookReq重命名的(shift + F6重命名就会自动重构所有相关大代码), 后者就是完全拷贝domain当中的内容。同时也记得将EbookResp重命名为EbookQueryResp即可。
另外,我们前端是使用Content-Type: application/json方式提交的post,所以后端需要使用@RequestBody这个注解参数。
接下来我们要去书写ebookService当中的save方法:
/*** 电子书保存接口(保存包括编辑保存->更新, 也包括新增保存->新增, 根据req是否有id来判断)* @param req 电子书保存参数*/public void save(EbookSaveReq req) {Ebook ebook = CopyUtil.copy(req, Ebook.class);if(ObjectUtils.isEmpty(req.getId())) {// 新增ebookMapper.insert(ebook);} else {// 更新// 因为updateByPrimaryKey传递的Ebook类型的参数,所以需要将EbookSaveReq 转化成EbookebookMapper.updateByPrimaryKey(ebook);}}
✿雪花算法和新增功能
1. 时间戳概念
时间戳是一串数字,比如1609805322846这种长整型,而1970-01-01 08:00:00这种不是时间戳,是能算日期格式化。
时间戳是怎么计算的呢,就是按照当前时间和1970-01-01 08:00:00的时间差值做当前时间的时间戳,下面是两种得到当前时间的时间戳的方法:
System.currentTimeMillis(); // 1609805322846new Date().getTime(); // 1609805322847
2. 雪花算法工具类
工具类,是用来生成数据库id的,雪花算法其实就是一个时间戳加上一些机器码,再加递增的序列号,按照下面的代码,我们是计算的是从2021年起始的时间,然后一毫秒内可以计算出2的12次方个时间戳,使用方法也很简单,使用snowFlake.nextId()即可
package com.taopoppy.wiki.util;import org.springframework.stereotype.Component;import java.text.ParseException;/*** Twitter的分布式自增ID雪花算法**/@Componentpublic class SnowFlake {/*** 起始的时间戳*/private final static long START_STMP = 1609459200000L; // 2021-01-01 00:00:00/*** 每一部分占用的位数*/private final static long SEQUENCE_BIT = 12; //序列号占用的位数private final static long MACHINE_BIT = 5; //机器标识占用的位数private final static long DATACENTER_BIT = 5;//数据中心占用的位数/*** 每一部分的最大值*/private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);/*** 每一部分向左的位移*/private final static long MACHINE_LEFT = SEQUENCE_BIT;private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;private long datacenterId = 1; //数据中心private long machineId = 1; //机器标识private long sequence = 0L; //序列号private long lastStmp = -1L;//上一次时间戳public SnowFlake() {}public SnowFlake(long datacenterId, long machineId) {if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");}if (machineId > MAX_MACHINE_NUM || machineId < 0) {throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");}this.datacenterId = datacenterId;this.machineId = machineId;}/*** 产生下一个ID** @return*/public synchronized long nextId() {long currStmp = getNewstmp();if (currStmp < lastStmp) {throw new RuntimeException("Clock moved backwards. Refusing to generate id");}if (currStmp == lastStmp) {//相同毫秒内,序列号自增sequence = (sequence + 1) & MAX_SEQUENCE;//同一毫秒的序列数已经达到最大if (sequence == 0L) {currStmp = getNextMill();}} else {//不同毫秒内,序列号置为0sequence = 0L;}lastStmp = currStmp;return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分| datacenterId << DATACENTER_LEFT //数据中心部分| machineId << MACHINE_LEFT //机器标识部分| sequence; //序列号部分}private long getNextMill() {long mill = getNewstmp();while (mill <= lastStmp) {mill = getNewstmp();}return mill;}private long getNewstmp() {return System.currentTimeMillis();}public static void main(String[] args) throws ParseException {// 时间戳// System.out.println(System.currentTimeMillis());// System.out.println(new Date().getTime());//// String dateTime = "2021-01-01 08:00:00";// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");// System.out.println(sdf.parse(dateTime).getTime());SnowFlake snowFlake = new SnowFlake(1, 1);long start = System.currentTimeMillis();for (int i = 0; i < 10; i++) {System.out.println(snowFlake.nextId());System.out.println(System.currentTimeMillis() - start);}}}
上面在main函数当中书写了一些测试代码,是通过new方法,但是我们希望通过注入的方式来使用,所以使用了@Component进行标注,使用的时候可以写@Resource,也可以写@Autowired,@Resource是jdk自带的,@Autowired是Spring自带的。
3. 完成新增功能
可以看到我们下面在新增的时候使用了id,id有几种算法,一种是最简单的自增,还有一种是uuid,还有一种就是雪花算法。三种算法的区别可以在网络上查一下。
@Servicepublic class EbookService {private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);@Resourceprivate EbookMapper ebookMapper;@Resourceprivate SnowFlake snowFlake;public void save(EbookSaveReq req) {Ebook ebook = CopyUtil.copy(req, Ebook.class);if(ObjectUtils.isEmpty(req.getId())) {// 新增, 给新增的数据添加雪花idebook.setId(snowFlake.nextId());ebookMapper.insert(ebook);} else {ebookMapper.updateByPrimaryKey(ebook);}}}
而前端只需要自己添加一个新增的按钮,然后给新增的按钮添加@click=add()即可:
// 新增const add = () => {modalVisible.value = true;ebook.value = {};};
电子书删除功能
1. 后端代码展示
删除一般都是按照id来删除,因为id是主键,我们就按照id来删除:
@DeleteMapping("/delete/{id}")public CommonResp delete(@PathVariable long id) {CommonResp resp = new CommonResp<>();ebookService.delete(id);return resp;}
/*** 电子书删除接口(按照id进行删除)* @param id*/public void delete(long id) {ebookMapper.deleteByPrimaryKey(id);}
注意我们上面是使用了method为delete方法,这些都是restful风格方法当中的一个。下面我们回到前端看看点击删除按钮如何请求前端的删除接口:
2. 前端代码展示
const handleDelete = (id: number) => {axios.delete("/ebook/delete/" + id).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加载列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};
✿集成Validation参数校验
这里的校验主要针对于电子书的查询和保存参数校验,使用到spring-boot-starter-validation这个插件,校验不通过的时候,前端弹出错误提示:
1. 集成validation
在pom.xml集成下面的代码,这个依赖是SpringBoot内置的,所以不需要加版本号:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
2. 后端集成校验
集成完毕我们不需要像nodejs那么去自己书写校验规则,而是直接到请求的参数类当中去添加注解,因为我们查询电子书的接口请求的参数类是EbookQueryReq,而且是继承了PageReq,由于考虑到查询必须要传递请求的页数和每页的条数,所以我们在PageReq当中给参数直接做注解
public class PageReq {@NotNull(message = "【页码】不能为空")private int page;@NotNull(message = "【每页条数】不能为空")@Max(value = 1000, message = "【每页条数】不能超过100")private int size;}
然后我们需要在校验的参数要添加@Valid的注解,表明这个参数是需要校验的,校验的规则就在EbookQueryReq或者EbookQueryReq继承的父类当中:
@GetMapping("/list")public CommonResp list(@Valid EbookQueryReq req) {}
这样配置好了之后,如果你传递的参数有错,就会报错,报org.springframework.validation.BindException的错误,但是参数如果校验错误,后端就无法正常的返回,直接返回400错误,而我们需要后端返回的是CommonResp,然后CommonResp当中的success为false即可,所以我们在controller当中创建一个ControllerExceptionHandler.java文件,然后内容如下
/*** 统一异常处理、数据预处理等*/@ControllerAdvicepublic class ControllerExceptionHandler {private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);/*** 校验异常统一处理* @param e* @return*/@ExceptionHandler(value = BindException.class)@ResponseBodypublic CommonResp validExceptionHandler(BindException e) {CommonResp commonResp = new CommonResp();LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());commonResp.setSuccess(false);commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());return commonResp;}}
这个文件会被springboot自动识别,因为有@ControllerAdvice的注解,上面的这个代码专门对BindException类型错误进行处理,如果要对其他类型错误进行处理,再写一个类似的函数。可以看到它返回的是CommonResp,设置一些属性,这样前端就能正确接收CommonResp,CommonResp当中包含着错误信息,而不是前端直接受到BindException错误。
管理功能优化
1. 增加名字查询
这个功能我们在后端已经实现过了,就是那个动态sql的实现,而前端我们去代码当中看一下vue3当中怎么书写的即可,这里不做过多的说明,去admin-ebook.vue当中查看查询的按钮即可。
2. 编辑时复制对象
在admin-ebook.vue当中,有编辑的功能,点击编辑会弹出一个框,这个表单框和列表数据是同一个数据,修改的时候就会去同步修改列表的数据,我们希望表单框的数据是复制一份出来的,所以我们需要深拷贝:
/*** 编辑*/const edit = (record: any) => {modalVisible.value = true;ebook.value = Tool.copy(record);categoryIds.value = [ebook.value.category1Id, ebook.value.category2Id]};
export class Tool {/*** 空校验 null或""都返回true*/public static isEmpty (obj: any) {if ((typeof obj === 'string')) {return !obj || obj.replace(/\s+/g, "") === ""} else {return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);}}/*** 非空校验*/public static isNotEmpty (obj: any) {return !this.isEmpty(obj);}/*** 对象复制* @param obj*/public static copy (obj: object) {if (Tool.isNotEmpty(obj)) {return JSON.parse(JSON.stringify(obj));}}}
