手把手撸一个wiki系统(4):电子书列表

2021年6月15日14:57:34
评论
45 10763字

上一节(手把手撸一个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会自动扫描并使用该类。

您可能感兴趣的文章

匿名

发表评论

匿名网友