第五章 电子书管理功能开发

## 1.增加电子书管理页面
⭐增加一个页面需要增加三个部分的内容：
①页面 ②菜单（点击菜单才会跳到页面） ③路由（路由跟页面是绑定起来的）
⭐增加页面，是在views下
改页面名称：对应的路由也要改router 
#### ①增加电子书管理页面（登录之后才可以操作）：views-admin-admin-ebook
```java
<template>
  <div class="about">
    <h1>电子书管理</h1>
  </div>
</template>
```
#### ②增加路由：router -index文件中
```java
import AdminEbook from '../views/admin/admin-ebook.vue'

    path: '/admin/ebook',
    name: 'AdminEbook',
    component: AdminEbook

```
#### ③增加菜单：components-the.header.vue中
```java
<a-menu-item key="/admin/ebook">
        <router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
```
跳转用router-link to ，点击电子书管理就会跳到/admin/ebook
key和to的值是一样的

## 2.电子书表格展示(admin-ebook.vue)
#### ①改造页面，修改布局：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/8f24b307-051e-8b1e-3ae3-fd2f60b49e5e.png)


```java
<template>
  <a-layout>
    <a-layout-content
      :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <div class="about">
        <h1>电子书管理</h1>
      </div>
    </a-layout-content>
  </a-layout>
</template>
```
#### ②改造管理页面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/d1f0f155-31e5-3abc-293e-483eed3e7b19.png)
在image下保存两张作为封面的测试，在数据库中写死路径
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/351ed8ae-1489-1f05-fe05-ae0f2107d50c.png)
```java
<template>
  <a-layout>
    <a-layout-content
      :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
    >
      <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:action="{ text, record }">
          <a-space size="small">
            <a-button type="primary">
              编辑
            </a-button>
            <a-button type="danger">
              删除
            </a-button>
          </a-space>
        </template>
      </a-table>
    </a-layout-content>
  </a-layout>
</template>

<script lang="ts">
  import { defineComponent, onMounted, ref } from 'vue';
  import axios from 'axios';

  export default defineComponent({
    name: 'AdminEbook',
    setup() {
      const ebooks = ref();
      const pagination = ref({
        current: 1,
        pageSize: 2,
        total: 0
      });
      const loading = ref(false);

      const columns = [
        {
          title: '封面',
          dataIndex: 'cover',
          slots: { customRender: 'cover' }
        },
        {
          title: '名称',
          dataIndex: 'name'
        },
        {
          title: '分类一',
          key: 'category1Id',
          dataIndex: 'category1Id'
        },
        {
          title: '分类二',
          dataIndex: 'category2Id'
        },
        {
          title: '文档数',
          dataIndex: 'docCount'
        },
        {
          title: '阅读数',
          dataIndex: 'viewCount'
        },
        {
          title: '点赞数',
          dataIndex: 'voteCount'
        },
        {
          title: 'Action',
          key: 'action',
          slots: { customRender: 'action' }
        }
      ];

      /**
       * 数据查询
       **/
      const handleQuery = (params: any) => {
        loading.value = true;
        axios.get("/ebook/list", params).then((response) => {
          loading.value = false;
          const data = response.data;
          ebooks.value = data.content;

          // 重置分页按钮
          pagination.value.current = params.page;
        });
      };

      /**
       * 表格点击页码时触发
       */
      const handleTableChange = (pagination: any) => {
        console.log("看看自带的分页参数都有啥：" + pagination);
        handleQuery({
          page: pagination.current,
          size: pagination.pageSize
        });
      };

      onMounted(() => {
        handleQuery({});
      });

      return {
        ebooks,
        pagination,
        columns,
        loading,
        handleTableChange
      }
    }
  });
</script>

<style scoped>
  img {
    width: 50px;
    height: 50px;
  }
</style>
```
在eslintrc中
```java
'vue/no-unused-vars': 0,
```

## 3.使用PageHelper实现后端分页
#### ①集成PageHelper插件
在pom文件中 增加依赖
```java
<!--pagehelper 插件-->
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper-spring-boot-starter</artifactId>
			<version>1.2.13</version>
		</dependency>
```


#### ②修改电子书列表接口，支持分页（假分页数据）
⭐分页最重要的四个元素：两个请求参数，两个返回参数
前端传过来的参数（要查第几页 第几条）， 返回给前端的参数（列表 total）
⭐分页最基本要查两次：一次是总数据，一次是当前页的列表数据
⭐⭐在EbookService中
```java
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);

PageHelper.startPage(1, 3);

PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数：{}", pageInfo.getTotal());
LOG.info("总页数：{}", pageInfo.getPages());
```
⭐⭐在application.properties文件中
将mapper层的日志级别设置为trace,最低级别，所有日志都会打印出来
```java
#打印所有的sql日志：sql,参数，结果
logging.level.com.jiawa.wiki.mapper=trace
```
## 4.封装分页请求参数和返回参数
⭐分页最重要的四个元素：两个请求参数，两个返回参数
前端传过来的参数（要查第几页 第几条）， 返回给前端的参数（列表 total）
将这四个参数封装为两个类
#### 请求参数封装PageReq
①req-PageReq:两个参数 页码和条数 get set tostring（打日志）方法
②EbookReq类继承PageReq类
③EbookService中:把参数变为动态的，而不是写死的
```java
PageHelper.startPage(req.getPage(), req.getSize());
```
#### 返回结果封装PageResp
①resp-PageResp:两个参数 total list列表（电子书，用户等等 不确定，用泛型，泛型的名字可以随意） get set tostring（打日志）方法
```java
private long total;
private List<T> list;
```
②EbookService中：返回整个PageResp<EbookResp>
```java
import com.jiawa.wiki.resp.PageResp;

public PageResp<EbookResp> list(EbookReq req) {

PageResp<EbookResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);

return pageResp;
```
③EbookController
```java
import com.jiawa.wiki.resp.PageResp;

CommonResp<PageResp<EbookResp>> resp = new CommonResp<>();
PageResp<EbookResp> list = ebookService.list(req);
```
## 5.前后端分页功能整合
#### ①电子书管理页面前后端分页整合
admin-ebook.vue中：
```java
pageSize: 4,

axios.get("/ebook/list", {
          params: {
            page: params.page,
            size: params.size
          }
        }).then((response) => {

ebooks.value = data.content.list;

pagination.value.total = data.content.total;

handleQuery({
          page: 1,
          size: pagination.value.pageSize,
        });

```
#### ②首页显示电子书列表，使用固定分页参数，一次性查出所有的电子书
home.vue中：
```java
     axios.get("/ebook/list", {
        params: {
          page: 1,
          size: 1000
        }
     }).then((response) => {


    ebooks.value = data.content.list;
```
## 6.制作电子书表单
⭐return:将用到的变量，方法返回给html
#### ①点击每一行编辑按钮，弹出编辑框
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/22e3986c-7de1-0c18-0a8f-d7a232a54692.png)
在eslintrc.js中：
```java
'@typescript-eslint/no-unused-vars'
```
在admin-ebook.vue中：
```java
<a-button type="primary" @click="edit">

<a-modal
    title="电子书表单"
    v-model:visible="modalVisible"
    :confirm-loading="modalLoading"+    @ok="handleModalOk"
  >
    <p>test</p>
</a-modal>

 -------- 表单 ---------
      const modalVisible = ref(false);
      const modalLoading = ref(false);
      const handleModalOk = () => {
        modalLoading.value = true;
        setTimeout(() => {
          modalVisible.value = false;
          modalLoading.value = false;
        }, 2000);
      };

      /**
       * 编辑
      */
      const edit = () => {+        modalVisible.value = true;
      };


    handleTableChange,
    edit,
    modalVisible,
    modalLoading,
    handleModalOk
```





#### ②编辑框显示电子书表单
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/dd1ad8c4-af5b-4619-549b-ad29773b28ec.png)
在admin-ebook.vue中：
```java
<a-button type="primary" @click="edit(record)">

<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-input v-model:value="ebook.category1Id" />
      </a-form-item>
      <a-form-item label="分类二">
        <a-input v-model:value="ebook.category2Id" />
      </a-form-item>
      <a-form-item label="描述">
        <a-input v-model:value="ebook.desc" type="textarea" />
      </a-form-item>
</a-form>

const ebook = ref({});

const edit = (record: any) => {

ebook.value = record

ebook,
```
## 7.完成电子书编辑功能
⭐保存类，更新类用Post，查询用get
RequestBody:json方式提交
#### ①增加后端保存接口（ebook保存接口）
#### ②点击保存时，调用保存接口
#### ③保存成功刷新列表
在EbookController中：
```java
com.jiawa.wiki.req.EbookQueryReq;
com.jiawa.wiki.req.EbookSaveReq;
com.jiawa.wiki.resp.EbookQueryResp;
org.springframework.web.bind.annotation.*;

public CommonResp list(EbookQueryReq req) {
        CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
        PageResp<EbookQueryResp> list = ebookService.list(req);

 @PostMapping("/save")
 public CommonResp save(@RequestBody EbookSaveReq req) {
        CommonResp resp = new CommonResp<>();
        ebookService.save(req);
        return resp;
```
在EbookReq中：
```java
public class EbookQueryReq extends PageReq {
```
新建类--EbookSaveReq：
```java
package com.jiawa.wiki.req;

public class EbookSaveReq {
    private Long id;

    private String name;

    private Long category1Id;

    private Long category2Id;

    private String description;

    private String cover;

    private Integer docCount;

    private Integer viewCount;

    private Integer voteCount;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getCategory1Id() {
        return category1Id;
    }

    public void setCategory1Id(Long category1Id) {
        this.category1Id = category1Id;
    }

    public Long getCategory2Id() {
        return category2Id;
    }

    public void setCategory2Id(Long category2Id) {
        this.category2Id = category2Id;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getCover() {
        return cover;
    }

    public void setCover(String cover) {
        this.cover = cover;
    }

    public Integer getDocCount() {
        return docCount;
    }

    public void setDocCount(Integer docCount) {
        this.docCount = docCount;
    }

    public Integer getViewCount() {
        return viewCount;
    }

    public void setViewCount(Integer viewCount) {
        this.viewCount = viewCount;
    }

    public Integer getVoteCount() {
        return voteCount;
    }

    public void setVoteCount(Integer voteCount) {
        this.voteCount = voteCount;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", name=").append(name);
        sb.append(", category1Id=").append(category1Id);
        sb.append(", category2Id=").append(category2Id);
        sb.append(", description=").append(description);
        sb.append(", cover=").append(cover);
        sb.append(", docCount=").append(docCount);
        sb.append(", viewCount=").append(viewCount);
        sb.append(", voteCount=").append(voteCount);
        sb.append("]");
        return sb.toString();
    }
}
```
在EbookResp中：
```java
public class EbookQueryResp {
```
在EbookService中：
```java
import com.jiawa.wiki.req.EbookQueryReq;
import com.jiawa.wiki.req.EbookSaveReq;
import com.jiawa.wiki.resp.EbookQueryResp;

public PageResp<EbookQueryResp> list(EbookQueryReq req) {

List<EbookQueryResp> list = CopyUtil.copyList(ebookList, EbookQueryResp.class);

PageResp<EbookQueryResp> pageResp = new PageResp();

/**
     * 保存
     */
    public void save(EbookSaveReq req) {
        Ebook ebook = CopyUtil.copy(req, Ebook.class);
        if (ObjectUtils.isEmpty(req.getId())) {
            // 新增
            ebookMapper.insert(ebook);
        } else {
            // 更新
            ebookMapper.updateByPrimaryKey(ebook);
        }
   }
```
在admin-ebook中：
```java
   axios.post("/ebook/save", ebook.value).then((response) => {
          const data = response.data; // data = commonResp
          if (data.success) {
            modalVisible.value = false;
            modalLoading.value = false;

            // 重新加载列表
            handleQuery({
              page: pagination.value.current,
              size: pagination.value.pageSize,
            });
          }
        });
```
## 8.雪花算法与新增功能
⭐雪花算法:是一个工具类，用来生成数据库id
在EbookServic中：
```java
import com.jiawa.wiki.util.SnowFlake;


@Resource 
private SnowFlake snowFlake;

ebook.setId(snowFlake.nextId());
```
在util下新建雪花类SnowFlake：
```java
package com.jiawa.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);
        }
    }
}
```
在admin-ebook中：
```java
<p>
        <a-button type="primary" @click="add()" size="large">
          新增
        </a-button>
</p>

/**
       * 新增
       */
      const add = () => {
        modalVisible.value = true;
        ebook.value = {};
      };

       add,
```
## 9.增加删除电子书功能
⭐电子书管理页面，点击某一行的删除按钮时，删除该行电子书
后端增加删除接口，前端点击删除按钮时调用后端删除接口，删除时需要有一个确认框
在EbookController中：
```java
@DeleteMapping("/delete/{id}")
    public CommonResp delete(@PathVariable Long id) {
        CommonResp resp = new CommonResp<>();
        ebookService.delete(id);
        return resp;
    }
```
在EbookService中：
```java
public void delete(Long id) {
        ebookMapper.deleteByPrimaryKey(id);
    }
```
在admin-ebook中：
```java
 <a-popconfirm
              title="删除后不可恢复，确认删除?"
              ok-text="是"
              cancel-text="否"
              @confirm="handleDelete(record.id)"
            >
              <a-button type="danger">
                删除
              </a-button>
</a-popconfirm>

<a-input v-model:value="ebook.description" type="textarea" />

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,
            });
          }
        });
      };


       handleModalOk,

       handleDelete
```
## 10.集成Validation做参数校验
对电子书查询和保存做参数校验
⭐集成spring-boot-starter-validation
⭐对保存接口和查询接口增加参数校验
⭐校验不通过时，前端弹出错误提示
#### ①集成Validation，为列表接口增加参数校验
在pom文件中增加依赖:
```java
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```
在controller下新增ControllerExceptionHandler类
```java
package com.jiawa.wiki.controller;

import com.jiawa.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 统一异常处理、数据预处理等
 */
@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;
    }
}
```
在EbookController中：
```java
import javax.validation.Valid;

public CommonResp list(@Valid EbookQueryReq req) {
```
在PageReq中：
```java
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;

@NotNull(message = "【页码】不能为空")

@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
```
在admin-ebook中：
```java
import { message } from 'ant-design-vue';

pageSize: 1001,

if (data.success) {
     ebooks.value = data.content.list;

// 重置分页按钮
           pagination.value.current = params.page;
           pagination.value.total = data.content.total;
          } else {
            message.error(data.message);
          }
```
#### ②为保存接口增加参数校验
在EbookController中：
```java
public CommonResp save(@Valid @RequestBody EbookSaveReq req) {
```
在EbookSaveReq中：
```java
import javax.validation.constraints.NotNull;

@NotNull(message = "【名称】不能为空")
```
在admin-ebook中：
```java
pageSize: 10,

modalLoading.value = false;

} else {
    message.error(data.message);
```
## 11.电子书管理功能优化
#### ①增加按名字查询电子书功能
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3899383/be797afb-3df6-04ad-825f-7273b5b7d7f0.png)
在admin-ebook中：
```java
<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>


const param = ref();
param.value = {};

size: params.size,
name: param.value.name

param,
handleQuery,
```
#### ②编辑时复制对象，修改表单时，不会影响列表数据
在util下新建tool.ts
```java
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));
    }
  }
}
```
在admin-ebook中：
```java
import {Tool} from "@/util/tool";

ebook.value = Tool.copy(record);
```