上一节(手把手撸一个wiki系统(3):前后端交互)我们进行了基础的前后端整合,这节编写一下具体的业务代码。
新建ebookAdmin页面
views下新建admin包,新建ebookAdmin.vue,内容:
<template>
<div class="ebookAdmin">
<h1>ebookAdmin</h1>
</div>
</template>
配置一下页面的路由,在router包下的index.ts里:
import About from '../views/About.vue'
import EbookAdmin from '../views/admin/ebookAdmin.vue'
...
{
path: '/admin/ebook',
name: 'EbookAdmin',
component: EbookAdmin
},
修改一下头部header,导向正确的页面:
<a-menu-item key="home">
<router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="ebookAdmin">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
见commit4-1.
添加分页pageHelper
依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
在ebookService的search方法加一行:
PageHelper.startPage(1,3);
查询的页数和查询的条数。
只对下方第一个遇到的sql起作用。
可以通过这三行代码获取总行数和总页数:
PageInfo<Object> pageInfo = new PageInfo<>(ebookList);
pageInfo.getTotal();
pageInfo.getPages();
封装分页请求、返回参数
request包下新建PageReq,内容:
package com.example.mywiki.request;
/**
* 分页请求参数封装
*/
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继承该类。
这样请求的时候可以带上参数了:?page=1&&size=4
返回参数要将总行数封装好,不然前端没法根据总行数获取页码。
response包新建PageResp,内容:
package com.example.mywiki.response;
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();
}
}
改造service:
public PageResp<EbookResp> search(EbookReq req) { if (!ObjectUtils.isEmpty(req)) { PageHelper.startPage(req.getPage(),req.getSize()); List<Ebook> ebookList = ebookMapper.selectByName("%" + req.getName() + "%"); //将List<Ebook>转换为List<EbookResp> List<EbookResp> respList = CopyUtil.copyList(ebookList, EbookResp.class); PageInfo<Ebook> ebookPageInfo = new PageInfo<>(ebookList); PageResp<EbookResp> pageResp = new PageResp<>(); pageResp.setTotal(ebookPageInfo.getTotal()); pageResp.setList(respList); return pageResp; } else { return null; }}
改造controller:
@GetMapping("/search")public CommonResp search(EbookReq req){ CommonResp<PageResp<EbookResp>> commonResp = new CommonResp<>(); PageResp<EbookResp> ebook = ebookService.search(req); commonResp.setContent(ebook); return commonResp;}
见4-3.
前后端分页数据同步
前端按照分页每次差几条,那么后端就应该提供几条,而不是一次全查。
引入pageHelper:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.3.0</version></dependency>
ebookAdmin.vue里改造:
<script lang="ts">import { defineComponent, onMounted, ref } from 'vue';import axios from 'axios';import { message } from 'ant-design-vue';export default defineComponent({ name: 'AdminEbook', setup() { const param = ref(); param.value = {}; const ebooks = ref(); //每页参数 const pagination = ref({ current: 1, pageSize: 8, total: 0 }); const loading = ref(false); //列表数据 const columns = [ { title: '封面', dataIndex: 'cover', slots: { customRender: 'cover' } }, { title: '名称', dataIndex: 'name' }, { title: '分类', slots: { customRender: 'category' } }, { title: '文档数', dataIndex: 'docCount' }, { title: '阅读数', dataIndex: 'viewCount' }, { title: '点赞数', dataIndex: 'voteCount' }, { title: 'Action', key: 'action', slots: { customRender: 'action' } } ]; /** * 数据查询 **/ const handleQuery = (params: any) => { loading.value = true; // 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据 ebooks.value = []; //将params的内容作为get请求参数,向后端controller请求 axios.get("/ebook/search", { 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 }); }; //初始执行,应该先查第一页,获得的数据传入params onMounted(() => { handleQuery({ page: 1, size: pagination.value.pageSize, }); }); return { param, ebooks, pagination, columns, loading, handleTableChange, handleQuery, } }});</script>
service改造:
public PageResp<EbookResp> search(EbookReq req) { //启用PageHelper,分页查询 PageHelper.startPage(req.getPage(), req.getSize()); List<Ebook> ebookList = ebookMapper.selectByName(req.getName()); //将List<Ebook>转换为List<EbookResp> List<EbookResp> respList = CopyUtil.copyList(ebookList, EbookResp.class); //获取分页信息,将total和List给pageResp PageInfo<Ebook> ebookPageInfo = new PageInfo<>(ebookList); PageResp<EbookResp> pageResp = new PageResp<>(); pageResp.setTotal(ebookPageInfo.getTotal()); pageResp.setList(respList); return pageResp; }
xml引入动态sql,name为空时查全部,name有值时模糊查询。被pageHelper限制:
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from ebook <if test="name != null and name != ''"> where name like concat('%',#{name},'%') </if></select>
Home.vue同样对写法改造:
setup(){ const ebooks = reactive({books:[]}); axios.get("/ebook/search",{ params:{ page:1, size:1000, } }).then((response) => { const data = response.data; ebooks.books = data.content.list; });
雪花算法生成id
utils包下SnowFlake类:
package com.example.mywiki.utils;import org.springframework.stereotype.Component;import java.text.ParseException;/** * Twitter的分布式自增ID雪花算法 * Created by tangssst@qq.com on 2021/06/13 */@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;//数据中心占用的位数,2的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); } }}
完成电子书保存/新增功能
controller:
@PostMapping("/save")public CommonResp save(@RequestBody EbookSaveReq req){ CommonResp commonResp = new CommonResp<>(); ebookService.save(req); return commonResp;}
service:
/** * Ebook保存save,传入的id无值是新增,id有值是更新 * @param saveReq */public void save(EbookSaveReq saveReq){ Ebook ebook = CopyUtil.copy(saveReq,Ebook.class); if (ObjectUtils.isEmpty(saveReq.getId())){ ebookMapper.insert(ebook); }else { ebookMapper.updateByPrimaryKey(ebook); }}
将原来的EbookReq和EbookResp改名为:EbookQueryReq,EbookQueryResp
见4-5
完成电子书删除功能
controller:
/** * 电子书删除 * @param id * @return */@DeleteMapping("/delete/{id}")public CommonResp delete(@PathVariable Long id){ CommonResp commonResp = new CommonResp<>(); ebookService.delete(id); return commonResp;}
service:
/** * Ebook删除 * @param id */public void delete(Long id){ ebookMapper.deleteByPrimaryKey(id);}
vue:
<a-popconfirm title="删除后不可恢复,确认删除?" ok-text="是" cancel-text="否" @confirm="handleDelete(record.id)"> <a-button type="danger"> 删除 </a-button></a-popconfirm>
/** * 删除 */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); } });};
见4-6
集成validation参数校验
依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId></dependency>
在PageReq里使用注解对size参数做限制:
//页码
@NotNull(message = "页码不能为空")
private int page;
//条数
@NotNull(message = "每页的条数不能为空")
@Max(value = 1000,message = "最大查询数:1000")
private int size;
EbookSaveReq:
@NotNull(message = "名称不能为空")
private String name;
controller出加个@Valid开启校验
@GetMapping("/search")
public CommonResp search(@Valid EbookQueryReq req){
CommonResp<PageResp<EbookQueryResp>> commonResp = new CommonResp<>();
PageResp<EbookQueryResp> ebook = ebookService.search(req);
commonResp.setContent(ebook);
return commonResp;
}
统一异常处理
controller新建ControllerExceptionHandler:
/**
* 统一异常处理、数据预处理
* Created by tangssst@qq.com on 2021/06/14
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
/**
* 校验异常统一处理,此处处理BindException
* @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会自动扫描并使用该类。