电子书管理功能开发

增加电子书管理页面

  • 增加电子书路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// component: () => import(/* webpackChunkName: "about" */ '../views/about.vue')
}
]
  • 增加电子书页面

    1
    2
    3
    4
    5
    <template>
    <div class="about">
    <h1>电子书管理</h1>
    </div>
    </template>
  • 增加电子书页面路由

    1
    2
    3
    4
    5
    {
    path: '/admin/ebook',
    name: 'AdminEbook',
    component: AdminEbook
    },
  • 增加电子书菜单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<a-layout-header class="header">
<div class="logo" />
<a-menu
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="/">
<router-link to="/">首页</router-link>
</a-menu-item>
<a-menu-item key="/admin/ebook">
<router-link to="/admin/ebook">电子书管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
</a-menu>
</a-layout-header>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
name: 'the-header'
});
</script>

电子书表格展示

熟悉表格组件的各种应用场景表格渲染
slots: 自定义渲染
title: 表头渲染
customRender: 值渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<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>

使用PageHelper实现后端分页

  • 集成PageHelper插件
    1
    2
    3
    4
    5
    6
    <!-- pagehelper 插件-->
    <dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.13</version>
    </dependency>
  • 使用PageHelper
    1
    PageHelper.startPage(page, size)

page从0开始,还是从1开始?size为0,或为空时会怎么样?

  • 获取分页信息

    1
    2
    3
    PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
    pageInfo.getTotal();
    pageInfo.getPages();
  • PageHelper原理:Mybatis拦截器,拦截到SQL后,增加limit关键

  • 字只对第一个遇到的SQL起作用!!!

  • 自定义Live Template

  • 打印所有的sql日志:sql, 参数, 结果

    1
    logging.level.com.jiawa.wiki.mapper=trace

封装分页请求参数和返回参数

  • 请求参数封装,PageReq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.javami.wiki.req;

/**
* @author Kevin
* @create 2021-10-21 14:15
*/
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();
}
}
  • 返回结果封装,PageResp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.javami.wiki.resp;

/**
* @author Kevin
* @create 2021-10-21 14:17
*/
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();
}
}

前后端分页功能整合

axios发送get请求参数的两种写法

  • 拼接url
  • 使用固定的params参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const handleQuery = (params: any) => {
loading.value = true;
axios.get("/ebook/list", {
params: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;

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

制作电子书表单

  • 弹出框组件、表单组件的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<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" @click="edit(record)">
编辑
</a-button>
<a-button type="danger">
删除
</a-button>
</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-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>
</a-modal>
</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: 4,
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: {
page: params.page,
size: params.size
}
}).then((response) => {
loading.value = false;
const data = response.data;
ebooks.value = data.content.list;

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

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

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

/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
ebook.value = record
};

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

return {
ebooks,
pagination,
columns,
loading,
handleTableChange,

edit,

ebook,
modalVisible,
modalLoading,
handleModalOk
}
}
});
</script>

<style scoped>
img {
width: 50px;
height: 50px;
}
</style>

完成电子书编辑功能

  • 保存接口请求参数用XXXSaveReq
    查询接口请求参数XXXQueryReq
    查询接口返回参数用XXXQueryResp

  • POST请求,如果是以json方式提交,后端参数需要增加@RequestBody,如果是以form方式提交,则不需要加任何注解

雪花算法与新增功能

  • 时间戳:当前时间(北京时间)与1970-01-01 08:00:00的毫秒时间差

  • 雪花算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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);
}
}
}

增加删除电子书功能

  • @DeleteMapping
  • 使用@PathVariable获取url上的参数
  • 气泡确认框

集成Validation做参数校验

  • 所有前端校验都是不靠谱的
    前端校验可以减少服务端压力

  • 需要引入依赖:

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

使用方法:

  • 实体增加校验注解
    @NotNull(message = “【每页条数】不能为空”)
    @Max(value = 100, message = “【每页条数】不能超过100”)
    protected Integer size;

  • 接口参数增加注解@Valid,开启校验
    public CommonResp query(@Valid EbookQueryReq req)
    全局异常处理,使用@ControllerAdvice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.javami.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;
}
}

电子书管理功能优化

js对象复制:将json对象转为json字符串,再转回json对象

web/src/util/tool.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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));
}
}
}