概述
- 增加电子书管理页面:页面,路由,菜单
- 电子书列表展示:表格组件,查询表单,后端列表查询接口
- 前后端分页处理:后端分页插件,前端分页组件,前后端分页参数传递
- 电子书编辑:模态框组件,表单组件,后端保存接口
- 电子书新增:界面复用编辑功能,后端复用保存接口,雪花算法
- 电子书删除:确认组件,后端删除接口
- 参数校验:集成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-popconfirm
title="删除后不可恢复,确认删除?"
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前面使用PageHelper
List<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);
@Resource
private 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); // PageInfo
LOG.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;
}
@Override
public 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;
}
@Override
public 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-popconfirm
title="删除后不可恢复,确认删除?"
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-modal
title="电子书表单"
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-cascader
v-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 = commonResp
if (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 转化成Ebook
ebookMapper.updateByPrimaryKey(ebook);
}
}
✿雪花算法和新增功能
1. 时间戳概念
时间戳是一串数字,比如1609805322846这种长整型,而1970-01-01 08:00:00这种不是时间戳,是能算日期格式化。
时间戳是怎么计算的呢,就是按照当前时间和1970-01-01 08:00:00的时间差值做当前时间的时间戳,下面是两种得到当前时间的时间戳的方法:
System.currentTimeMillis(); // 1609805322846
new 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雪花算法
**/
@Component
public 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 {
//不同毫秒内,序列号置为0
sequence = 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,还有一种就是雪花算法。三种算法的区别可以在网络上查一下。
@Service
public class EbookService {
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);
@Resource
private EbookMapper ebookMapper;
@Resource
private SnowFlake snowFlake;
public void save(EbookSaveReq req) {
Ebook ebook = CopyUtil.copy(req, Ebook.class);
if(ObjectUtils.isEmpty(req.getId())) {
// 新增, 给新增的数据添加雪花id
ebook.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 = commonResp
if (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文件,然后内容如下
/**
* 统一异常处理、数据预处理等
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public 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));
}
}
}