知识库系统开发记录(二)
十二、分类管理功能开发
分类表设计与代码生成
分类表设计:项目设计两级分类,表设计支持无限级分类
all.sql
# 分类
drop table if exists `category`;
create table `category` (
`id` bigint not null comment 'id',
`parent` bigint not null default 0 comment '父id',
`name` varchar(50) not null comment '名称',
`sort` int comment '顺序',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='分类';
insert into `category` (id, parent, name, sort) values (100, 000, '前端开发', 100);
insert into `category` (id, parent, name, sort) values (101, 100, 'Vue', 101);
insert into `category` (id, parent, name, sort) values (102, 100, 'HTML & CSS', 102);
insert into `category` (id, parent, name, sort) values (200, 000, 'Java', 200);
insert into `category` (id, parent, name, sort) values (201, 200, '基础应用', 201);
insert into `category` (id, parent, name, sort) values (202, 200, '框架应用', 202);
insert into `category` (id, parent, name, sort) values (300, 000, 'Python', 300);
insert into `category` (id, parent, name, sort) values (301, 300, '基础应用', 301);
insert into `category` (id, parent, name, sort) values (302, 300, '进阶方向应用', 302);
insert into `category` (id, parent, name, sort) values (400, 000, '数据库', 400);
insert into `category` (id, parent, name, sort) values (401, 400, 'MySQL', 401);
insert into `category` (id, parent, name, sort) values (500, 000, '其它', 500);
insert into `category` (id, parent, name, sort) values (501, 500, '服务器', 501);
insert into `category` (id, parent, name, sort) values (502, 500, '开发工具', 502);
insert into `category` (id, parent, name, sort) values (503, 500, '热门服务端语言', 503);
生成持久层代码 - mybatis-generator
generator-config.xml
<!-- <table tableName="ebook"/>-->
<table tableName="category"/>
完成分类基本增删改查功能
- 按照电子书管理,复制出一套分类管理的代码
CategoryController.java
@RestController
@RequestMapping("/category")
public class CategoryController {
@Resource
private CategoryService categoryService;
@GetMapping("/list")
public CommonResp list(@Valid CategoryQueryReq req) {
CommonResp<PageResp<CategoryQueryResp>> resp = new CommonResp<>();
PageResp<CategoryQueryResp> list = categoryService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody CategorySaveReq req) {
CommonResp resp = new CommonResp<>();
categoryService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
categoryService.delete(id);
return resp;
}
}
CategoryQueryReq.java
public class CategoryQueryReq extends PageReq {
@Override
public String toString() {
return "CategoryQueryReq{} " + super.toString();
}
}
CategorySaveReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
public class CategorySaveReq {
private Long id;
private Long parent;
@NotNull(message = "【名称】不能为空")
private String name;
@NotNull(message = "【排序】不能为空")
private Integer sort;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
@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(", parent=").append(parent);
sb.append(", name=").append(name);
sb.append(", sort=").append(sort);
sb.append("]");
return sb.toString();
}
}
CategoryQueryResp.java
package com.li.response;
public class CategoryQueryResp {
private Long id;
private Long parent;
private String name;
private Integer sort;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
@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(", parent=").append(parent);
sb.append(", name=").append(name);
sb.append(", sort=").append(sort);
sb.append("]");
return sb.toString();
}
}
CategoryService.java
@Service
public class CategoryService {
private static final Logger LOG = LoggerFactory.getLogger(CategoryService.class);
@Resource
private CategoryMapper categoryMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<CategoryQueryResp> list(CategoryQueryReq req) {
CategoryExample categoryExample = new CategoryExample();
CategoryExample.Criteria criteria = categoryExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
PageInfo<Category> pageInfo = new PageInfo<>(categoryList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
PageResp<CategoryQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(CategorySaveReq req) {
Category category = CopyUtil.copy(req, Category.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
category.setId(snowFlake.nextId());
categoryMapper.insert(category);
} else {
// 更新
categoryMapper.updateByPrimaryKey(category);
}
}
public void delete(Long id) {
categoryMapper.deleteByPrimaryKey(id);
}
}
the-header.vue
<a-menu-item key="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
index.ts
import { createRouter, createWebHistory} from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/about.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
import AdminCategory from '../views/admin/admin-category.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook
},
{
path: '/admin/category',
name: 'AdminCategory',
component: AdminCategory
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router
admin-category.vue
<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="categorys"
: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-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="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="category.name" />
</a-form-item>
<a-form-item label="父分类">
<a-input v-model:value="category.parent" />
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="category.sort" />
</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: 'AdminCategory',
setup() {
const param = ref();
param.value = {};
const categorys = ref();
const pagination = ref({
current: 1,
pageSize: 10,
total: 0
});
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父分类',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 数据查询
**/
const handleQuery = (params: any) => {
loading.value = true;
axios.get("http://127.0.0.1:8080/category/list", {
params: {
page: params.page,
size: params.size,
name: param.value.name
}
}).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys.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
});
};
// -------- 表单 ---------
const category = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/category/save", category.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;
category.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
category.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/category/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
param,
categorys,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
edit,
add,
category,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
分类表格显示优化
不需要分页,一次查出全部数据
CategoryController.java
@GetMapping("/all")
public CommonResp all() {
CommonResp<List<CategoryQueryResp>> resp = new CommonResp<>();
List<CategoryQueryResp> list = categoryService.all();
resp.setContent(list);
return resp;
}
CategoryService.java
public List<CategoryQueryResp> all() {
CategoryExample categoryExample = new CategoryExample();
categoryExample.setOrderByClause("sort asc");
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
return list;
}
public PageResp<CategoryQueryResp> list(CategoryQueryReq req) {
CategoryExample categoryExample = new CategoryExample();
categoryExample.setOrderByClause("sort asc");
CategoryExample.Criteria criteria = categoryExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
PageInfo<Category> pageInfo = new PageInfo<>(categoryList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
PageResp<CategoryQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
修改admin-category.vue
<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-button type="primary" @click="handleQuery()">
查询
</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="categorys"
:loading="loading"
:pagination="false"
>
<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-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="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="category.name" />
</a-form-item>
<a-form-item label="父分类">
<a-input v-model:value="category.parent" />
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="category.sort" />
</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: 'AdminCategory',
setup() {
const param = ref();
param.value = {};
const categorys = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父分类',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys.value = data.content;
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const category = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/category/save", category.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
category.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
category.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/category/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
onMounted(() => {
handleQuery();
});
return {
param,
categorys,
columns,
loading,
handleQuery,
edit,
add,
category,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
改为树型表格展示
使用递归将数组转为树形结构 - tool.ts
/**
* 使用递归将数组转为树形结构
* 父ID属性为parent
*/
public static array2Tree (array: any, parentId: number) {
if (Tool.isEmpty(array)) {
return [];
}
const result = [];
for (let i = 0; i < array.length; i++) {
const c = array[i];
// console.log(Number(c.parent), Number(parentId));
if (Number(c.parent) === Number(parentId)) {
result.push(c);
// 递归查看当前节点对应的子节点
const children = Tool.array2Tree(array, c.id);
if (Tool.isNotEmpty(children)) {
c.children = children;
}
}
}
return result;
}
修改admin-category.vue
- 一级分类树,children属性就是二级分类
<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-button type="primary" @click="handleQuery()">
查询
</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="level1"
:loading="loading"
:pagination="false"
>
<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-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="category" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="category.name" />
</a-form-item>
<a-form-item label="父分类">
<a-input v-model:value="category.parent" />
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="category.sort" />
</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: 'AdminCategory',
setup() {
const param = ref();
param.value = {};
const categorys = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父分类',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级分类树,children属性就是二级分类
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级分类树,children属性就是二级分类
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys.value = data.content;
console.log("原始数组:", categorys.value);
level1.value = [];
level1.value = Tool.array2Tree(categorys.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const category = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/category/save", category.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
category.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
category.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/category/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// categorys,
level1,
columns,
loading,
handleQuery,
edit,
add,
category,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
分类编辑功能优化
编辑(新增/修改)分类时,支持选中某一分类作为父分类,或无父分类
修改admin-category.vue
<a-form-item label="父分类">
<a-select
v-model:value="category.parent"
ref="select"
>
<a-select-option value="0">
无
</a-select-option>
<a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="category.id === c.id">
{{c.name}}
</a-select-option>
</a-select>
</a-form-item>
测试
电子书管理增加分类选择
电子书管理页面,使用级联选择组件Cascader,选择分类
电子书列表应该显示分类名称,而不是分类ID
修改admin-ebook.vue
<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">
<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() {
const param = ref();
param.value = {};
const ebooks = ref();
const pagination = ref({
current: 1,
pageSize: 10,
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;
axios.get("http://127.0.0.1:8080/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
});
};
// -------- 表单 ---------
/**
* 数组,[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("http://127.0.0.1:8080/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]
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/ebook/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
loading.value = true;
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
} else {
message.error(data.message);
}
});
};
const getCategoryName = (cid: number) => {
// console.log(cid)
let result = "";
categorys.forEach((item: any) => {
if (item.id === cid) {
// return item.name; // 注意,这里直接return不起作用
result = item.name;
}
});
return result;
};
onMounted(() => {
handleQueryCategory();
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
param,
ebooks,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
getCategoryName,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk,
categoryIds,
level1,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
首页显示分类菜单
首页显示分类菜单
修改home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
@click="handleClick"
>
<a-menu-item key="welcome">
<router-link :to="'/'">
<MailOutlined />
<span>欢迎</span>
</router-link>
</a-menu-item>
<a-sub-menu v-for="item in level1" :key="item.id">
<template v-slot:title>
<span><user-outlined />{{item.name}}</span>
</template>
<a-menu-item v-for="child in item.children" :key="child.id">
<MailOutlined /><span>{{child.name}}</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks">
<template #renderItem="{ item }">
<a-list-item key="item.name">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</template>
<template #avatar><a-avatar :src="item.cover"/></template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'Home',
setup() {
const ebooks = ref();
// const ebooks1 = reactive({books: []});
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
} else {
message.error(data.message);
}
});
};
const handleClick = () => {
console.log("menu click")
};
onMounted(() => {
handleQueryCategory();
axios.get("http://127.0.0.1:8080/ebook/list", {
params: {
page: 1,
size: 1000
}
}).then((response) => {
const data = response.data;
ebooks.value = data.content.list;
// ebooks1.books = data.content;
});
});
return {
ebooks,
// ebooks2: toRef(ebooks1, "books"),
// listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
handleClick,
level1,
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
加载完分类后,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错
修改admin-ebook.vue
<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">
<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() {
const param = ref();
param.value = {};
const ebooks = ref();
const pagination = ref({
current: 1,
pageSize: 10,
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 = [];
axios.get("http://127.0.0.1:8080/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
});
};
// -------- 表单 ---------
/**
* 数组,[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("http://127.0.0.1:8080/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]
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
ebook.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/ebook/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
loading.value = true;
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
// 加载完分类后,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
} else {
message.error(data.message);
}
});
};
const getCategoryName = (cid: number) => {
// console.log(cid)
let result = "";
categorys.forEach((item: any) => {
if (item.id === cid) {
// return item.name; // 注意,这里直接return不起作用
result = item.name;
}
});
return result;
};
onMounted(() => {
handleQueryCategory();
});
return {
param,
ebooks,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
getCategoryName,
edit,
add,
ebook,
modalVisible,
modalLoading,
handleModalOk,
categoryIds,
level1,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
点击分类菜单显示电子书
首页默认显示欢迎页面 - 点击欢迎时,显示欢迎组件,点击分类时,显示电子书
修改home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
@click="handleClick"
>
<a-menu-item key="welcome">
<MailOutlined />
<span>欢迎</span>
</a-menu-item>
<a-sub-menu v-for="item in level1" :key="item.id">
<template v-slot:title>
<span><user-outlined />{{item.name}}</span>
</template>
<a-menu-item v-for="child in item.children" :key="child.id">
<MailOutlined /><span>{{child.name}}</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<div class="welcome" v-show="isShowWelcome">
<h1>欢迎使用知识库</h1>
</div>
<a-list v-show="!isShowWelcome" item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks">
<template #renderItem="{ item }">
<a-list-item key="item.name">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</template>
<template #avatar><a-avatar :src="item.cover"/></template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'Home',
setup() {
const ebooks = ref();
// const ebooks1 = reactive({books: []});
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
} else {
message.error(data.message);
}
});
};
const isShowWelcome = ref(true);
const handleClick = (value: any) => {
// console.log("menu click", value)
// if (value.key === 'welcome') {
// isShowWelcome.value = true;
// } else {
// isShowWelcome.value = false;
// }
isShowWelcome.value = value.key === 'welcome';
};
onMounted(() => {
handleQueryCategory();
axios.get("http://127.0.0.1:8080/ebook/list", {
params: {
page: 1,
size: 1000
}
}).then((response) => {
const data = response.data;
ebooks.value = data.content.list;
// ebooks1.books = data.content;
});
});
return {
ebooks,
// ebooks2: toRef(ebooks1, "books"),
// listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
handleClick,
level1,
isShowWelcome
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
点击某分类时,显示该分类下的电子书 - 点击分类时,重新查询电子书 - 电子书后端接口增加分类参数
修改EbookQueryReq.java
package com.li.request;
public class EbookQueryReq extends PageReq {
private Long id;
private String name;
private Long categoryId2;
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 getCategoryId2() {
return categoryId2;
}
public void setCategoryId2(Long categoryId2) {
this.categoryId2 = categoryId2;
}
@Override
public String toString() {
return "EbookQueryReq{" +
"id=" + id +
", name='" + name + '\'' +
", categoryId2=" + categoryId2 +
"} " + super.toString();
}
}
EbookService新增判断**if** (!ObjectUtils.isEmpty(req.getCategoryId2()))
if (!ObjectUtils.isEmpty(req.getCategoryId2())) {
criteria.andCategory2IdEqualTo(req.getCategoryId2());
}
修改home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
@click="handleClick"
>
<a-menu-item key="welcome">
<MailOutlined />
<span>欢迎</span>
</a-menu-item>
<a-sub-menu v-for="item in level1" :key="item.id">
<template v-slot:title>
<span><user-outlined />{{item.name}}</span>
</template>
<a-menu-item v-for="child in item.children" :key="child.id">
<MailOutlined /><span>{{child.name}}</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<div class="welcome" v-show="isShowWelcome">
<h1>欢迎使用知识库</h1>
</div>
<a-list v-show="!isShowWelcome" item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks">
<template #renderItem="{ item }">
<a-list-item key="item.name">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</template>
<template #avatar><a-avatar :src="item.cover"/></template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'Home',
setup() {
const ebooks = ref();
// const ebooks1 = reactive({books: []});
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
axios.get("http://127.0.0.1:8080/category/all").then((response) => {
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
} else {
message.error(data.message);
}
});
};
const isShowWelcome = ref(true);
let categoryId2 = 0;
const handleQueryEbook = () => {
axios.get("http://127.0.0.1:8080/ebook/list", {
params: {
page: 1,
size: 1000,
categoryId2: categoryId2
}
}).then((response) => {
const data = response.data;
ebooks.value = data.content.list;
// ebooks1.books = data.content;
});
};
const handleClick = (value: any) => {
// console.log("menu click", value)
if (value.key === 'welcome') {
isShowWelcome.value = true;
} else {
categoryId2 = value.key;
isShowWelcome.value = false;
handleQueryEbook();
}
// isShowWelcome.value = value.key === 'welcome';
};
onMounted(() => {
handleQueryCategory();
// handleQueryEbook();
});
return {
ebooks,
// ebooks2: toRef(ebooks1, "books"),
// listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
actions: [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
],
handleClick,
level1,
isShowWelcome
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
测试
十三、文档管理功能开发
文档表设计与代码生成
文档表设计
-- 文档表
drop table if exists `doc`;
create table `doc` (
`id` bigint not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`parent` bigint not null default 0 comment '父id',
`name` varchar(50) not null comment '名称',
`sort` int comment '顺序',
`view_count` int default 0 comment '阅读数',
`vote_count` int default 0 comment '点赞数',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='文档';
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (1, 1, 0, '文档1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (2, 1, 1, '文档1.1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (3, 1, 0, '文档2', 2, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (4, 1, 3, '文档2.1', 1, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (5, 1, 3, '文档2.2', 2, 0, 0);
insert into `doc` (id, ebook_id, parent, name, sort, view_count, vote_count) values (6, 1, 5, '文档2.2.1', 1, 0, 0);
生成持久层代码
Doc.java
package com.li.entity;
public class Doc {
private Long id;
private Long ebookId;
private Long parent;
private String name;
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
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(", ebookId=").append(ebookId);
sb.append(", parent=").append(parent);
sb.append(", name=").append(name);
sb.append(", sort=").append(sort);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append("]");
return sb.toString();
}
}
DocExample.java
package com.li.entity;
import java.util.ArrayList;
import java.util.List;
public class DocExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public DocExample() {
oredCriteria = new ArrayList<>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(Long value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(Long value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(Long value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(Long value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(Long value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(Long value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<String> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<Long> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(Long value1, Long value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(Long value1, Long value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andEbookIdIsNull() {
addCriterion("ebook_id is null");
return (Criteria) this;
}
public Criteria andEbookIdIsNotNull() {
addCriterion("ebook_id is not null");
return (Criteria) this;
}
public Criteria andEbookIdEqualTo(Long value) {
addCriterion("ebook_id =", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotEqualTo(Long value) {
addCriterion("ebook_id <>", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdGreaterThan(Long value) {
addCriterion("ebook_id >", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdGreaterThanOrEqualTo(Long value) {
addCriterion("ebook_id >=", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdLessThan(Long value) {
addCriterion("ebook_id <", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdLessThanOrEqualTo(Long value) {
addCriterion("ebook_id <=", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdIn(List<Long> values) {
addCriterion("ebook_id in", values, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotIn(List<Long> values) {
addCriterion("ebook_id not in", values, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdBetween(Long value1, Long value2) {
addCriterion("ebook_id between", value1, value2, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotBetween(Long value1, Long value2) {
addCriterion("ebook_id not between", value1, value2, "ebookId");
return (Criteria) this;
}
public Criteria andParentIsNull() {
addCriterion("parent is null");
return (Criteria) this;
}
public Criteria andParentIsNotNull() {
addCriterion("parent is not null");
return (Criteria) this;
}
public Criteria andParentEqualTo(Long value) {
addCriterion("parent =", value, "parent");
return (Criteria) this;
}
public Criteria andParentNotEqualTo(Long value) {
addCriterion("parent <>", value, "parent");
return (Criteria) this;
}
public Criteria andParentGreaterThan(Long value) {
addCriterion("parent >", value, "parent");
return (Criteria) this;
}
public Criteria andParentGreaterThanOrEqualTo(Long value) {
addCriterion("parent >=", value, "parent");
return (Criteria) this;
}
public Criteria andParentLessThan(Long value) {
addCriterion("parent <", value, "parent");
return (Criteria) this;
}
public Criteria andParentLessThanOrEqualTo(Long value) {
addCriterion("parent <=", value, "parent");
return (Criteria) this;
}
public Criteria andParentIn(List<Long> values) {
addCriterion("parent in", values, "parent");
return (Criteria) this;
}
public Criteria andParentNotIn(List<Long> values) {
addCriterion("parent not in", values, "parent");
return (Criteria) this;
}
public Criteria andParentBetween(Long value1, Long value2) {
addCriterion("parent between", value1, value2, "parent");
return (Criteria) this;
}
public Criteria andParentNotBetween(Long value1, Long value2) {
addCriterion("parent not between", value1, value2, "parent");
return (Criteria) this;
}
public Criteria andNameIsNull() {
addCriterion("`name` is null");
return (Criteria) this;
}
public Criteria andNameIsNotNull() {
addCriterion("`name` is not null");
return (Criteria) this;
}
public Criteria andNameEqualTo(String value) {
addCriterion("`name` =", value, "name");
return (Criteria) this;
}
public Criteria andNameNotEqualTo(String value) {
addCriterion("`name` <>", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThan(String value) {
addCriterion("`name` >", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThanOrEqualTo(String value) {
addCriterion("`name` >=", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThan(String value) {
addCriterion("`name` <", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThanOrEqualTo(String value) {
addCriterion("`name` <=", value, "name");
return (Criteria) this;
}
public Criteria andNameLike(String value) {
addCriterion("`name` like", value, "name");
return (Criteria) this;
}
public Criteria andNameNotLike(String value) {
addCriterion("`name` not like", value, "name");
return (Criteria) this;
}
public Criteria andNameIn(List<String> values) {
addCriterion("`name` in", values, "name");
return (Criteria) this;
}
public Criteria andNameNotIn(List<String> values) {
addCriterion("`name` not in", values, "name");
return (Criteria) this;
}
public Criteria andNameBetween(String value1, String value2) {
addCriterion("`name` between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andNameNotBetween(String value1, String value2) {
addCriterion("`name` not between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andSortIsNull() {
addCriterion("sort is null");
return (Criteria) this;
}
public Criteria andSortIsNotNull() {
addCriterion("sort is not null");
return (Criteria) this;
}
public Criteria andSortEqualTo(Integer value) {
addCriterion("sort =", value, "sort");
return (Criteria) this;
}
public Criteria andSortNotEqualTo(Integer value) {
addCriterion("sort <>", value, "sort");
return (Criteria) this;
}
public Criteria andSortGreaterThan(Integer value) {
addCriterion("sort >", value, "sort");
return (Criteria) this;
}
public Criteria andSortGreaterThanOrEqualTo(Integer value) {
addCriterion("sort >=", value, "sort");
return (Criteria) this;
}
public Criteria andSortLessThan(Integer value) {
addCriterion("sort <", value, "sort");
return (Criteria) this;
}
public Criteria andSortLessThanOrEqualTo(Integer value) {
addCriterion("sort <=", value, "sort");
return (Criteria) this;
}
public Criteria andSortIn(List<Integer> values) {
addCriterion("sort in", values, "sort");
return (Criteria) this;
}
public Criteria andSortNotIn(List<Integer> values) {
addCriterion("sort not in", values, "sort");
return (Criteria) this;
}
public Criteria andSortBetween(Integer value1, Integer value2) {
addCriterion("sort between", value1, value2, "sort");
return (Criteria) this;
}
public Criteria andSortNotBetween(Integer value1, Integer value2) {
addCriterion("sort not between", value1, value2, "sort");
return (Criteria) this;
}
public Criteria andViewCountIsNull() {
addCriterion("view_count is null");
return (Criteria) this;
}
public Criteria andViewCountIsNotNull() {
addCriterion("view_count is not null");
return (Criteria) this;
}
public Criteria andViewCountEqualTo(Integer value) {
addCriterion("view_count =", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotEqualTo(Integer value) {
addCriterion("view_count <>", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountGreaterThan(Integer value) {
addCriterion("view_count >", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountGreaterThanOrEqualTo(Integer value) {
addCriterion("view_count >=", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountLessThan(Integer value) {
addCriterion("view_count <", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountLessThanOrEqualTo(Integer value) {
addCriterion("view_count <=", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountIn(List<Integer> values) {
addCriterion("view_count in", values, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotIn(List<Integer> values) {
addCriterion("view_count not in", values, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountBetween(Integer value1, Integer value2) {
addCriterion("view_count between", value1, value2, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotBetween(Integer value1, Integer value2) {
addCriterion("view_count not between", value1, value2, "viewCount");
return (Criteria) this;
}
public Criteria andVoteCountIsNull() {
addCriterion("vote_count is null");
return (Criteria) this;
}
public Criteria andVoteCountIsNotNull() {
addCriterion("vote_count is not null");
return (Criteria) this;
}
public Criteria andVoteCountEqualTo(Integer value) {
addCriterion("vote_count =", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotEqualTo(Integer value) {
addCriterion("vote_count <>", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountGreaterThan(Integer value) {
addCriterion("vote_count >", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountGreaterThanOrEqualTo(Integer value) {
addCriterion("vote_count >=", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountLessThan(Integer value) {
addCriterion("vote_count <", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountLessThanOrEqualTo(Integer value) {
addCriterion("vote_count <=", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountIn(List<Integer> values) {
addCriterion("vote_count in", values, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotIn(List<Integer> values) {
addCriterion("vote_count not in", values, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountBetween(Integer value1, Integer value2) {
addCriterion("vote_count between", value1, value2, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotBetween(Integer value1, Integer value2) {
addCriterion("vote_count not between", value1, value2, "voteCount");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}
DocMapper.java
package com.li.mapper;
import com.li.entity.Doc;
import com.li.entity.DocExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface DocMapper {
long countByExample(DocExample example);
int deleteByExample(DocExample example);
int deleteByPrimaryKey(Long id);
int insert(Doc record);
int insertSelective(Doc record);
List<Doc> selectByExample(DocExample example);
Doc selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("record") Doc record, @Param("example") DocExample example);
int updateByExample(@Param("record") Doc record, @Param("example") DocExample example);
int updateByPrimaryKeySelective(Doc record);
int updateByPrimaryKey(Doc record);
}
generator-config.xml
<!--<table tableName="category"/>-->
+ <table tableName="doc"/>
完成文档表基本增删改查功能
- 按照分类管理,复制出一套文档管理的代码。
DocController.java
package com.li.controller;
import com.li.request.DocQueryReq;
import com.li.request.DocSaveReq;
import com.li.response.CommonResp;
import com.li.response.DocQueryResp;
import com.li.response.PageResp;
import com.li.service.DocService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/doc")
public class DocController {
@Resource
private DocService docService;
@GetMapping("/all")
public CommonResp all() {
CommonResp<List<DocQueryResp>> resp = new CommonResp<>();
List<DocQueryResp> list = docService.all();
resp.setContent(list);
return resp;
}
@GetMapping("/list")
public CommonResp list(@Valid DocQueryReq req) {
CommonResp<PageResp<DocQueryResp>> resp = new CommonResp<>();
PageResp<DocQueryResp> list = docService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody DocSaveReq req) {
CommonResp resp = new CommonResp<>();
docService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
docService.delete(id);
return resp;
}
}
DocQueryReq.java
public class DocQueryReq extends PageReq {
@Override
public String toString() {
return "DocQueryReq{} " + super.toString();
}
}
DocSaveReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
public class DocSaveReq {
private Long id;
@NotNull(message = "【电子书】不能为空")
private Long ebookId;
@NotNull(message = "【父文档】不能为空")
private Long parent;
@NotNull(message = "【名称】不能为空")
private String name;
@NotNull(message = "【顺序】不能为空")
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
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(", ebookId=").append(ebookId);
sb.append(", parent=").append(parent);
sb.append(", name=").append(name);
sb.append(", sort=").append(sort);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append("]");
return sb.toString();
}
}
DocQueryResp.java
package com.li.response;
public class DocQueryResp {
private Long id;
private Long ebookId;
private Long parent;
private String name;
private Integer sort;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
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(", ebookId=").append(ebookId);
sb.append(", parent=").append(parent);
sb.append(", name=").append(name);
sb.append(", sort=").append(sort);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append("]");
return sb.toString();
}
}
DocService.java
package com.li.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.li.entity.Doc;
import com.li.entity.DocExample;
import com.li.mapper.DocMapper;
import com.li.request.DocQueryReq;
import com.li.request.DocSaveReq;
import com.li.response.DocQueryResp;
import com.li.response.PageResp;
import com.li.utils.CopyUtil;
import com.li.utils.SnowFlake;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
import java.util.List;
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private SnowFlake snowFlake;
public List<DocQueryResp> all() {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
docMapper.insert(doc);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
}
index.ts
import AdminDoc from '../views/admin/admin-doc.vue'
{
path: '/admin/doc',
name: 'AdminDoc',
component: AdminDoc
},
admin-doc.vue
<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-button type="primary" @click="handleQuery()">
查询
</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="level1"
:loading="loading"
:pagination="false"
>
<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-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="doc" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="doc.name" />
</a-form-item>
<a-form-item label="父文档">
<a-select
v-model:value="doc.parent"
ref="select"
>
<a-select-option value="0">
无
</a-select-option>
<a-select-option v-for="c in level1" :key="c.id" :value="c.id" :disabled="doc.id === c.id">
{{c.name}}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="doc.sort" />
</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: 'AdminDoc',
setup() {
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父文档',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const doc = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
doc.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/doc/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
admin-ebook.vue
<router-link to="/admin/doc">
<a-button type="primary">
文档管理
</a-button>
</router-link>
测试
使用树形选择组件选择父节点
- 编辑表单中的父文档选择框改为树形选择组件,完成编辑功能。
- 使用递归算法,将树形数据中,当前目标节点及其子孙节点设置成disabled解决编辑保存后,重新编辑时,数据不更新的问题为树形选择增加一个“无”选择,用来新增一级节点。
使用递归算法
admin-category.vue
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
admin-doc.vue
<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-button type="primary" @click="handleQuery()">
查询
</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="level1"
:loading="loading"
:pagination="false"
>
<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-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="doc" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="doc.name" />
</a-form-item>
<a-form-item label="父文档">
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="doc.sort" />
</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: 'AdminDoc',
setup() {
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父文档',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const doc = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
doc.value = {};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/doc/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleModalOk,
handleDelete,
treeSelectData
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
测试
Vue页面参数传递完成新增文档功能
修改admin-doc.vue
import {useRoute} from "vue-router";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
doc.value = {
ebookId: route.query.ebookId
};
修改router-link(admin-ebook.vue)
<!-- <router-link to="/admin/doc">-->
<router-link :to="'/admin/doc?ebookId=' + record.id">
增加删除文档功能
- 删除某个文档时,其下所有的文档也应该删除。
删除某个文档时,将其下所有的文档全部删除
修改DocController.java
@DeleteMapping("/delete/{idsStr}")
public CommonResp delete(@PathVariable String idsStr) {
CommonResp resp = new CommonResp<>();
List<String> list = Arrays.asList(idsStr.split(","));
docService.delete(list);
return resp;
}
修改DocExample.java
//public Criteria andIdIn(List<Long> values) {
public Criteria andIdIn(List<String> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
DocService.java
delete**(List<String> ids)
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
查找整根树枝 - admin-doc.vue
const ids: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
ids.push(id);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
getDeleteIds(level1.value, id);
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + ids.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
};
删除某个文档时,增加二次确认
Modal.confirm - 修改admin-doc.vue
<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-button type="primary" @click="handleQuery()">
查询
</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="level1"
:loading="loading"
:pagination="false"
>
<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-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="doc" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="doc.name" />
</a-form-item>
<a-form-item label="父文档">
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="doc.sort" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父文档',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const doc = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
const deleteIds: Array<string> = [];
const deleteNames: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
deleteIds.push(id);
deleteNames.push(node.name);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",
onOk() {
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + deleteIds.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
},
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleModalOk,
handleDelete,
treeSelectData
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
修复BUG:清空数组,否则多次删除时,数组会一直增加
Modal.confirm - 修改admin-doc.vue
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
+ // 清空数组,否则多次删除时,数组会一直增加
+ deleteIds.length = 0;
+ deleteNames.length = 0;
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
集成富文本插件WangEditor
package-lock.json
- @babel/runtime-corejs3
- core-js-pure
- wangeditor
"@babel/runtime-corejs3": {
"version": "7.12.5",
"resolved": "https://registry.npm.taobao.org/@babel/runtime-corejs3/download/@babel/runtime-corejs3-7.12.5.tgz?cache=0&sync_timestamp=1604444010370&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime-corejs3%2Fdownload%2F%40babel%2Fruntime-corejs3-7.12.5.tgz",
"integrity": "sha1-/+6R2g60xtrggHdOlLpgY2jkFPQ=",
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.4"
}
},
"core-js-pure": {
"version": "3.8.3",
"resolved": "https://registry.npm.taobao.org/core-js-pure/download/core-js-pure-3.8.3.tgz",
"integrity": "sha1-EOnjslkuyu3kKD6POtcCCBFYfAI="
},
"wangeditor": {
"version": "4.6.3",
"resolved": "https://registry.npm.taobao.org/wangeditor/download/wangeditor-4.6.3.tgz?cache=0&sync_timestamp=1610628553498&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwangeditor%2Fdownload%2Fwangeditor-4.6.3.tgz",
"integrity": "sha1-zFcpEbIOMY/KglSvD+Nu/EBkK9w=",
"requires": {
"@babel/runtime": "^7.11.2",
"@babel/runtime-corejs3": "^7.11.2"
}
},
package.json
"vuex": "^4.0.0-0",
"wangeditor": "^4.6.3"
修改admin-doc.vue
<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-button type="primary" @click="handleQuery()">
查询
</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="level1"
:loading="loading"
:pagination="false"
>
<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-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="doc" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="名称">
<a-input v-model:value="doc.name" />
</a-form-item>
<a-form-item label="父文档">
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item label="顺序">
<a-input v-model:value="doc.sort" />
</a-form-item>
<a-form-item label="内容">
<div id="content"></div>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
import E from 'wangeditor'
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name'
},
{
title: '父文档',
key: 'parent',
dataIndex: 'parent'
},
{
title: '顺序',
dataIndex: 'sort'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const doc = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
const deleteIds: Array<string> = [];
const deleteNames: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
deleteIds.push(id);
deleteNames.push(node.name);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
setTimeout(function () {
editor.create();
}, 100);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
setTimeout(function () {
editor.create();
}, 100);
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
// 清空数组,否则多次删除时,数组会一直增加
deleteIds.length = 0;
deleteNames.length = 0;
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",
onOk() {
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + deleteIds.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
},
});
};
onMounted(() => {
handleQuery();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleModalOk,
handleDelete,
treeSelectData
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
文档内容表设计与代码生成
文档内容表设计
sql
-- 文档内容
drop table if exists `content`;
create table `content` (
`id` bigint not null comment '文档id',
`content` mediumtext not null comment '内容',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='文档内容';
生成持久层代码
Content.java
package com.li.entity;
public class Content {
private Long id;
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@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(", content=").append(content);
sb.append("]");
return sb.toString();
}
}
ContentExample.java
package com.li.entity;
import java.util.ArrayList;
import java.util.List;
public class ContentExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public ContentExample() {
oredCriteria = new ArrayList<>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(Long value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(Long value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(Long value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(Long value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(Long value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(Long value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<Long> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<Long> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(Long value1, Long value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(Long value1, Long value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}
ContentMapper.java
package com.li.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface ContentMapper {
long countByExample(ContentExample example);
int deleteByExample(ContentExample example);
int deleteByPrimaryKey(Long id);
int insert(Content record);
int insertSelective(Content record);
List<Content> selectByExampleWithBLOBs(ContentExample example);
List<Content> selectByExample(ContentExample example);
Content selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("record") Content record, @Param("example") ContentExample example);
int updateByExampleWithBLOBs(@Param("record") Content record, @Param("example") ContentExample example);
int updateByExample(@Param("record") Content record, @Param("example") ContentExample example);
int updateByPrimaryKeySelective(Content record);
int updateByPrimaryKeyWithBLOBs(Content record);
}
generator-config.xml
<!--<table tableName="doc"/>-->
<table tableName="content"/>
文档管理页面布局修改
- 将文档列表和表单变成左右布局
- 文档管理布局美化
- 让表格树默认展开
修改admin-doc.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-row :gutter="24">
<a-col :span="8">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleQuery()">
查询
</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
<a-table
v-if="level1.length > 0"
:columns="columns"
:row-key="record => record.id"
:data-source="level1"
:loading="loading"
:pagination="false"
size="small"
:defaultExpandAllRows="true"
>
<template #name="{ text, record }">
{{record.sort}} {{text}}
</template>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)" size="small">
编辑
</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger" size="small">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-col>
<a-col :span="16">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleSave()">
保存
</a-button>
</a-form-item>
</a-form>
</p>
<a-form :model="doc" layout="vertical">
<a-form-item>
<a-input v-model:value="doc.name" placeholder="名称"/>
</a-form-item>
<a-form-item>
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item>
<a-input v-model:value="doc.sort" placeholder="顺序"/>
</a-form-item>
<a-form-item>
<div id="content"></div>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
<!--<a-modal-->
<!-- title="文档表单"-->
<!-- v-model:visible="modalVisible"-->
<!-- :confirm-loading="modalLoading"-->
<!-- @ok="handleModalOk"-->
<!-->-->
<!-- -->
<!--</a-modal>-->
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
import E from 'wangeditor'
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
const columns = [
{
title: '名称',
dataIndex: 'name',
slots: { customRender: 'name' }
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const doc = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
editor.config.zIndex = 0;
const handleSave = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
const deleteIds: Array<string> = [];
const deleteNames: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
deleteIds.push(id);
deleteNames.push(node.name);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
// 清空数组,否则多次删除时,数组会一直增加
deleteIds.length = 0;
deleteNames.length = 0;
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",
onOk() {
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + deleteIds.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
},
});
};
onMounted(() => {
handleQuery();
editor.create();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleSave,
handleDelete,
treeSelectData
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
文档内容的保存与显示
- 文档内容的保存
- 前端获取富文本框的html字符串
- 保存文档接口里,增加内容参数,保存时同时保存文档和内容。
修改DocSaveReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
public class DocSaveReq {
private Long id;
@NotNull(message = "【电子书】不能为空")
private Long ebookId;
@NotNull(message = "【父文档】不能为空")
private Long parent;
@NotNull(message = "【名称】不能为空")
private String name;
@NotNull(message = "【顺序】不能为空")
private Integer sort;
private Integer viewCount;
private Integer voteCount;
@NotNull(message = "【内容】不能为空")
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Long getParent() {
return parent;
}
public void setParent(Long parent) {
this.parent = parent;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
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;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "DocSaveReq{" +
"id=" + id +
", ebookId=" + ebookId +
", parent=" + parent +
", name='" + name + '\'' +
", sort=" + sort +
", viewCount=" + viewCount +
", voteCount=" + voteCount +
", content='" + content + '\'' +
'}';
}
}
修改DocService.java
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
public List<DocQueryResp> all() {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// List<DocResp> respList = new ArrayList<>();
// for (Doc doc : docList) {
// // DocResp docResp = new DocResp();
// // BeanUtils.copyProperties(doc, docResp);
// // 对象复制
// DocResp docResp = CopyUtil.copy(doc, DocResp.class);
//
// respList.add(docResp);
// }
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
}
修改admin-doc.vue
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const doc = ref();
doc.value = {};
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
@@ -187,6 +188,7 @@
const handleSave = () => {
modalLoading.value = true;
doc.value.content = editor.txt.html();
axios.post("/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
文档内容的显示
- 文档内容的显示
- 增加单独获取内容的接口
- 前端得到html字符串后,放入富文本框中
增加读取文档内容功能
修改DocController.java
@GetMapping("/find-content/{id}")
public CommonResp findContent(@PathVariable Long id) {
CommonResp<String> resp = new CommonResp<>();
String content = docService.findContent(id);
resp.setContent(content);
return resp;
}
修改DocService.java
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
return content.getContent();
}
修改admin-doc.vue
/**
* 内容查询
**/
const handleQueryContent = () => {
axios.get("/doc/find-content/" + doc.value.id).then((response) => {
const data = response.data;
if (data.success) {
editor.txt.html(data.content)
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
modalVisible.value = true;
doc.value = Tool.copy(record);
handleQueryContent();
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
增加保存文档成功提示
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
modalVisible.value = false;
// modalVisible.value = false;
message.success("保存成功!");
// 重新加载列表
handleQuery();
@@ -287,6 +288,8 @@
* 编辑
*/
const edit = (record: any) => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = Tool.copy(record);
handleQueryContent();
@@ -303,6 +306,8 @@
* 新增
*/
const add = () => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
文档页面功能开发
增加文档页面,点击首页电子书可以进入文档页面
index.ts
import Doc from '../views/doc.vue'
{
path: '/doc',
name: 'Doc',
component: Doc
},
doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<div class="doc">
<h1>欢迎来到文档页面</h1>
</div>
</a-layout-content>
</a-layout>
</template>
修改home.vue
<!-- <a :href="item.href">{{ item.name }}</a>-->
<router-link :to="'/doc?ebookId=' + item.id">
{{ item.name }}
</router-link>
使用树形组件展示文档树
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
>
</a-tree>
</a-col>
<a-col :span="18">
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
const docs = ref();
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("http://127.0.0.1:8080/doc/all").then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
} else {
message.error(data.message);
}
});
};
onMounted(() => {
handleQuery();
});
return {
level1,
}
}
});
</script>
文档页面和文档管理页面,只能查当前电子书下所有的文档
修改DocController.java
@GetMapping("/all/{ebookId}")
public CommonResp all(@PathVariable Long ebookId) {
CommonResp<List<DocQueryResp>> resp = new CommonResp<>();
List<DocQueryResp> list = docService.all(ebookId);
resp.setContent(list);
return resp;
}
修改DocService.java
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
修改admin-doc.vue
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
} else {
message.error(data.message);
}
});
};
修改doc.vue
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
} else {
message.error(data.message);
}
});
};
Bug修复,打开编辑页面时,应该初始化父文档选择框,免去手动点新增
修改admin-doc.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-row :gutter="24">
<a-col :span="8">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleQuery()">
查询
</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
<a-table
v-if="level1.length > 0"
:columns="columns"
:row-key="record => record.id"
:data-source="level1"
:loading="loading"
:pagination="false"
size="small"
:defaultExpandAllRows="true"
>
<template #name="{ text, record }">
{{record.sort}} {{text}}
</template>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)" size="small">
编辑
</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger" size="small">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-col>
<a-col :span="16">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleSave()">
保存
</a-button>
</a-form-item>
</a-form>
</p>
<a-form :model="doc" layout="vertical">
<a-form-item>
<a-input v-model:value="doc.name" placeholder="名称"/>
</a-form-item>
<a-form-item>
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item>
<a-input v-model:value="doc.sort" placeholder="顺序"/>
</a-form-item>
<a-form-item>
<div id="content"></div>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
<!--<a-modal-->
<!-- title="文档表单"-->
<!-- v-model:visible="modalVisible"-->
<!-- :confirm-loading="modalLoading"-->
<!-- @ok="handleModalOk"-->
<!-->-->
<!-- -->
<!--</a-modal>-->
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
import E from 'wangeditor'
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const columns = [
{
title: '名称',
dataIndex: 'name',
slots: { customRender: 'name' }
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
// 父文档下拉框初始化,相当于点击新增
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const doc = ref();
doc.value = {};
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
editor.config.zIndex = 0;
const handleSave = () => {
modalLoading.value = true;
doc.value.content = editor.txt.html();
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
// modalVisible.value = false;
message.success("保存成功!");
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
const deleteIds: Array<string> = [];
const deleteNames: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
deleteIds.push(id);
deleteNames.push(node.name);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
/**
* 内容查询
**/
const handleQueryContent = () => {
axios.get("http://127.0.0.1:8080/doc/find-content/" + doc.value.id).then((response) => {
const data = response.data;
if (data.success) {
editor.txt.html(data.content)
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = Tool.copy(record);
handleQueryContent();
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
/**
* 新增
*/
const add = () => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
// 清空数组,否则多次删除时,数组会一直增加
deleteIds.length = 0;
deleteNames.length = 0;
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",
onOk() {
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + deleteIds.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
},
});
};
onMounted(() => {
handleQuery();
editor.create();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleSave,
handleDelete,
treeSelectData
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
Bug修复,如果文档内容表没数据,则查询内容表会出现空指针异常
修改DocService.java
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
点击文档节点时,加载并显示文档内容
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
>
</a-tree>
</a-col>
<a-col :span="18">
<div :innerHTML="html"></div>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
} else {
message.error(data.message);
}
});
};
/**
* 内容查询
**/
const handleQueryContent = (id: number) => {
axios.get("http://127.0.0.1:8080/doc/find-content/" + id).then((response) => {
const data = response.data;
if (data.success) {
html.value = data.content;
} else {
message.error(data.message);
}
});
};
const onSelect = (selectedKeys: any, info: any) => {
console.log('selected', selectedKeys, info);
if (Tool.isNotEmpty(selectedKeys)) {
// 加载内容
handleQueryContent(selectedKeys[0]);
}
};
onMounted(() => {
handleQuery();
});
return {
level1,
html,
onSelect
}
}
});
</script>
为文档内容增加wangeditor样式,和富文本框保持一致,所见即所得
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
>
</a-tree>
</a-col>
<a-col :span="18">
<div class="wangeditor" :innerHTML="html"></div>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
} else {
message.error(data.message);
}
});
};
/**
* 内容查询
**/
const handleQueryContent = (id: number) => {
axios.get("http://127.0.0.1:8080/doc/find-content/" + id).then((response) => {
const data = response.data;
if (data.success) {
html.value = data.content;
} else {
message.error(data.message);
}
});
};
const onSelect = (selectedKeys: any, info: any) => {
console.log('selected', selectedKeys, info);
if (Tool.isNotEmpty(selectedKeys)) {
// 加载内容
handleQueryContent(selectedKeys[0]);
}
};
onMounted(() => {
handleQuery();
});
return {
level1,
html,
onSelect
}
}
});
</script>
<style>
/* wangeditor默认样式, 参照: http://www.wangeditor.com/doc/pages/02-%E5%86%85%E5%AE%B9%E5%A4%84%E7%90%86/03-%E8%8E%B7%E5%8F%96html.html */
/* table 样式 */
.wangeditor table {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.wangeditor table td,
.wangeditor table th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 3px 5px;
}
.wangeditor table th {
border-bottom: 2px solid #ccc;
text-align: center;
}
/* blockquote 样式 */
.wangeditor blockquote {
display: block;
border-left: 8px solid #d0e5f2;
padding: 5px 10px;
margin: 10px 0;
line-height: 1.4;
font-size: 100%;
background-color: #f1f1f1;
}
/* code 样式 */
.wangeditor code {
display: inline-block;
*display: inline;
*zoom: 1;
background-color: #f1f1f1;
border-radius: 3px;
padding: 3px 5px;
margin: 0 3px;
}
.wangeditor pre code {
display: block;
}
/* ul ol 样式 */
.wangeditor ul, ol {
margin: 10px 0 10px 20px;
}
</style>
文档内容样式和antdv p冲突,覆盖掉
修改doc.vue
/* 和antdv p冲突,覆盖掉 */
.wangeditor blockquote p {
font-family:"YouYuan";
margin: 20px 10px !important;
font-size: 16px !important;
font-weight:600;
}
文档管理增加内容预览功能
修改admin-doc.vue
<template>
<a-layout>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-row :gutter="24">
<a-col :span="8">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleQuery()">
查询
</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
<a-table
v-if="level1.length > 0"
:columns="columns"
:row-key="record => record.id"
:data-source="level1"
:loading="loading"
:pagination="false"
size="small"
:defaultExpandAllRows="true"
>
<template #name="{ text, record }">
{{record.sort}} {{text}}
</template>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="edit(record)" size="small">
编辑
</a-button>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="是"
cancel-text="否"
@confirm="handleDelete(record.id)"
>
<a-button type="danger" size="small">
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
</a-col>
<a-col :span="16">
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-button type="primary" @click="handleSave()">
保存
</a-button>
</a-form-item>
</a-form>
</p>
<a-form :model="doc" layout="vertical">
<a-form-item>
<a-input v-model:value="doc.name" placeholder="名称"/>
</a-form-item>
<a-form-item>
<a-tree-select
v-model:value="doc.parent"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeSelectData"
placeholder="请选择父文档"
tree-default-expand-all
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
>
</a-tree-select>
</a-form-item>
<a-form-item>
<a-input v-model:value="doc.sort" placeholder="顺序"/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handlePreviewContent()">
<EyeOutlined /> 内容预览
</a-button>
</a-form-item>
<a-form-item>
<div id="content"></div>
</a-form-item>
</a-form>
</a-col>
</a-row>
<a-drawer width="900" placement="right" :closable="false" :visible="drawerVisible" @close="onDrawerClose">
<div class="wangeditor" :innerHTML="previewHtml"></div>
</a-drawer>
</a-layout-content>
</a-layout>
<!--<a-modal-->
<!-- title="文档表单"-->
<!-- v-model:visible="modalVisible"-->
<!-- :confirm-loading="modalLoading"-->
<!-- @ok="handleModalOk"-->
<!-->-->
<!-- -->
<!--</a-modal>-->
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message, Modal} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
import ExclamationCircleOutlined from "@ant-design/icons-vue/ExclamationCircleOutlined";
import E from 'wangeditor'
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
console.log("路由:", route);
console.log("route.path:", route.path);
console.log("route.query:", route.query);
console.log("route.param:", route.params);
console.log("route.fullPath:", route.fullPath);
console.log("route.name:", route.name);
console.log("route.meta:", route.meta);
const param = ref();
param.value = {};
const docs = ref();
const loading = ref(false);
// 因为树选择组件的属性状态,会随当前编辑的节点而变化,所以单独声明一个响应式变量
const treeSelectData = ref();
treeSelectData.value = [];
const columns = [
{
title: '名称',
dataIndex: 'name',
slots: { customRender: 'name' }
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 数据查询
**/
const handleQuery = () => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
level1.value = [];
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
docs.value = data.content;
console.log("原始数组:", docs.value);
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
console.log("树形结构:", level1);
// 父文档下拉框初始化,相当于点击新增
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
} else {
message.error(data.message);
}
});
};
// -------- 表单 ---------
const doc = ref();
doc.value = {};
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
editor.config.zIndex = 0;
const handleSave = () => {
modalLoading.value = true;
doc.value.content = editor.txt.html();
axios.post("http://127.0.0.1:8080/doc/save", doc.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
// modalVisible.value = false;
message.success("保存成功!");
// 重新加载列表
handleQuery();
} else {
message.error(data.message);
}
});
};
/**
* 将某节点及其子孙节点全部置为disabled
*/
const setDisable = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("disabled", node);
// 将目标节点设置为disabled
node.disabled = true;
// 遍历所有子节点,将所有子节点全部都加上disabled
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
setDisable(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
setDisable(children, id);
}
}
}
};
const deleteIds: Array<string> = [];
const deleteNames: Array<string> = [];
/**
* 查找整根树枝
*/
const getDeleteIds = (treeSelectData: any, id: any) => {
// console.log(treeSelectData, id);
// 遍历数组,即遍历某一层节点
for (let i = 0; i < treeSelectData.length; i++) {
const node = treeSelectData[i];
if (node.id === id) {
// 如果当前节点就是目标节点
console.log("delete", node);
// 将目标ID放入结果集ids
// node.disabled = true;
deleteIds.push(id);
deleteNames.push(node.name);
// 遍历所有子节点
const children = node.children;
if (Tool.isNotEmpty(children)) {
for (let j = 0; j < children.length; j++) {
getDeleteIds(children, children[j].id)
}
}
} else {
// 如果当前节点不是目标节点,则到其子节点再找找看。
const children = node.children;
if (Tool.isNotEmpty(children)) {
getDeleteIds(children, id);
}
}
}
};
/**
* 内容查询
**/
const handleQueryContent = () => {
axios.get("http://127.0.0.1:8080/doc/find-content/" + doc.value.id).then((response) => {
const data = response.data;
if (data.success) {
editor.txt.html(data.content)
} else {
message.error(data.message);
}
});
};
/**
* 编辑
*/
const edit = (record: any) => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = Tool.copy(record);
handleQueryContent();
// 不能选择当前节点及其所有子孙节点,作为父节点,会使树断开
treeSelectData.value = Tool.copy(level1.value);
setDisable(treeSelectData.value, record.id);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
/**
* 新增
*/
const add = () => {
// 清空富文本框
editor.txt.html("");
modalVisible.value = true;
doc.value = {
ebookId: route.query.ebookId
};
treeSelectData.value = Tool.copy(level1.value);
// 为选择树添加一个"无"
treeSelectData.value.unshift({id: 0, name: '无'});
};
const handleDelete = (id: number) => {
// console.log(level1, level1.value, id)
// 清空数组,否则多次删除时,数组会一直增加
deleteIds.length = 0;
deleteNames.length = 0;
getDeleteIds(level1.value, id);
Modal.confirm({
title: '重要提醒',
icon: createVNode(ExclamationCircleOutlined),
content: '将删除:【' + deleteNames.join(",") + "】删除后不可恢复,确认删除?",
onOk() {
// console.log(ids)
axios.delete("http://127.0.0.1:8080/doc/delete/" + deleteIds.join(",")).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery();
}
});
},
});
};
// ----------------富文本预览--------------
const drawerVisible = ref(false);
const previewHtml = ref();
const handlePreviewContent = () => {
const html = editor.txt.html();
previewHtml.value = html;
drawerVisible.value = true;
};
const onDrawerClose = () => {
drawerVisible.value = false;
};
onMounted(() => {
handleQuery();
editor.create();
});
return {
param,
// docs,
level1,
columns,
loading,
handleQuery,
edit,
add,
doc,
modalVisible,
modalLoading,
handleSave,
handleDelete,
treeSelectData,
drawerVisible,
previewHtml,
handlePreviewContent,
onDrawerClose,
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
打开文档页面时,默认选中第一个节点
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
:defaultSelectedKeys="defaultSelectedKeys"
>
</a-tree>
</a-col>
<a-col :span="18">
<div class="wangeditor" :innerHTML="html"></div>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'AdminDoc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
const defaultSelectedKeys = ref();
defaultSelectedKeys.value = [];
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 内容查询
**/
const handleQueryContent = (id: number) => {
axios.get("http://127.0.0.1:8080/doc/find-content/" + id).then((response) => {
const data = response.data;
if (data.success) {
html.value = data.content;
} else {
message.error(data.message);
}
});
};
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("http://127.0.0.1:8080/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
if (Tool.isNotEmpty(level1)) {
defaultSelectedKeys.value = [level1.value[0].id];
handleQueryContent(level1.value[0].id);
}
} else {
message.error(data.message);
}
});
};
const onSelect = (selectedKeys: any, info: any) => {
console.log('selected', selectedKeys, info);
if (Tool.isNotEmpty(selectedKeys)) {
// 加载内容
handleQueryContent(selectedKeys[0]);
}
};
onMounted(() => {
handleQuery();
});
return {
level1,
html,
onSelect,
defaultSelectedKeys
}
}
});
</script>
<style>
/* wangeditor默认样式, 参照: http://www.wangeditor.com/doc/pages/02-%E5%86%85%E5%AE%B9%E5%A4%84%E7%90%86/03-%E8%8E%B7%E5%8F%96html.html */
/* table 样式 */
.wangeditor table {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.wangeditor table td,
.wangeditor table th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 3px 5px;
}
.wangeditor table th {
border-bottom: 2px solid #ccc;
text-align: center;
}
/* blockquote 样式 */
.wangeditor blockquote {
display: block;
border-left: 8px solid #d0e5f2;
padding: 5px 10px;
margin: 10px 0;
line-height: 1.4;
font-size: 100%;
background-color: #f1f1f1;
}
/* code 样式 */
.wangeditor code {
display: inline-block;
*display: inline;
*zoom: 1;
background-color: #f1f1f1;
border-radius: 3px;
padding: 3px 5px;
margin: 0 3px;
}
.wangeditor pre code {
display: block;
}
/* ul ol 样式 */
.wangeditor ul, ol {
margin: 10px 0 10px 20px;
}
/* 和antdv p冲突,覆盖掉 */
.wangeditor blockquote p {
font-family:"YouYuan";
margin: 20px 10px !important;
font-size: 16px !important;
font-weight:600;
}
</style>
打开文档页面时,如果没有文档,增加提示
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<h3 v-if="level1.length === 0">对不起,找不到相关文档!</h3>
<a-row>
//-------------------------------
BUG修复,初始打开文档管理页面,直接新增文档时,会报错:电子书不能为空
修改admin-doc.vue
// -------- 表单 ---------
const doc = ref();
doc.value = {};
doc.value = {
ebookId: route.query.ebookId
};
const modalVisible = ref(false);
const modalLoading = ref(false);
const editor = new E('#content');
十四、用户管理&单点登录
用户表设计与持久层代码生成
用户表设计
sql
-- 用户表
drop table if exists `user`;
create table `user` (
`id` bigint not null comment 'ID',
`login_name` varchar(50) not null comment '登陆名',
`name` varchar(50) comment '昵称',
`password` char(32) not null comment '密码',
primary key (`id`),
unique key `login_name_unique` (`login_name`)
) engine=innodb default charset=utf8mb4 comment='用户';
insert into `user` (id, `login_name`, `name`, `password`) values (1, 'test', '测试', 'e70e2222a9d67c4f2eae107533359aa4');
生成持久层代码
User.java
package com.li.entity;
public class User {
private Long id;
private String loginName;
private String name;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@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(", loginName=").append(loginName);
sb.append(", name=").append(name);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
UserExample.java
package com.li.entity;
import java.util.ArrayList;
import java.util.List;
public class UserExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public UserExample() {
oredCriteria = new ArrayList<>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(Long value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(Long value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(Long value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(Long value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(Long value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(Long value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<Long> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<Long> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(Long value1, Long value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(Long value1, Long value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andLoginNameIsNull() {
addCriterion("login_name is null");
return (Criteria) this;
}
public Criteria andLoginNameIsNotNull() {
addCriterion("login_name is not null");
return (Criteria) this;
}
public Criteria andLoginNameEqualTo(String value) {
addCriterion("login_name =", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameNotEqualTo(String value) {
addCriterion("login_name <>", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameGreaterThan(String value) {
addCriterion("login_name >", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameGreaterThanOrEqualTo(String value) {
addCriterion("login_name >=", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameLessThan(String value) {
addCriterion("login_name <", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameLessThanOrEqualTo(String value) {
addCriterion("login_name <=", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameLike(String value) {
addCriterion("login_name like", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameNotLike(String value) {
addCriterion("login_name not like", value, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameIn(List<String> values) {
addCriterion("login_name in", values, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameNotIn(List<String> values) {
addCriterion("login_name not in", values, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameBetween(String value1, String value2) {
addCriterion("login_name between", value1, value2, "loginName");
return (Criteria) this;
}
public Criteria andLoginNameNotBetween(String value1, String value2) {
addCriterion("login_name not between", value1, value2, "loginName");
return (Criteria) this;
}
public Criteria andNameIsNull() {
addCriterion("`name` is null");
return (Criteria) this;
}
public Criteria andNameIsNotNull() {
addCriterion("`name` is not null");
return (Criteria) this;
}
public Criteria andNameEqualTo(String value) {
addCriterion("`name` =", value, "name");
return (Criteria) this;
}
public Criteria andNameNotEqualTo(String value) {
addCriterion("`name` <>", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThan(String value) {
addCriterion("`name` >", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThanOrEqualTo(String value) {
addCriterion("`name` >=", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThan(String value) {
addCriterion("`name` <", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThanOrEqualTo(String value) {
addCriterion("`name` <=", value, "name");
return (Criteria) this;
}
public Criteria andNameLike(String value) {
addCriterion("`name` like", value, "name");
return (Criteria) this;
}
public Criteria andNameNotLike(String value) {
addCriterion("`name` not like", value, "name");
return (Criteria) this;
}
public Criteria andNameIn(List<String> values) {
addCriterion("`name` in", values, "name");
return (Criteria) this;
}
public Criteria andNameNotIn(List<String> values) {
addCriterion("`name` not in", values, "name");
return (Criteria) this;
}
public Criteria andNameBetween(String value1, String value2) {
addCriterion("`name` between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andNameNotBetween(String value1, String value2) {
addCriterion("`name` not between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andPasswordIsNull() {
addCriterion("`password` is null");
return (Criteria) this;
}
public Criteria andPasswordIsNotNull() {
addCriterion("`password` is not null");
return (Criteria) this;
}
public Criteria andPasswordEqualTo(String value) {
addCriterion("`password` =", value, "password");
return (Criteria) this;
}
public Criteria andPasswordNotEqualTo(String value) {
addCriterion("`password` <>", value, "password");
return (Criteria) this;
}
public Criteria andPasswordGreaterThan(String value) {
addCriterion("`password` >", value, "password");
return (Criteria) this;
}
public Criteria andPasswordGreaterThanOrEqualTo(String value) {
addCriterion("`password` >=", value, "password");
return (Criteria) this;
}
public Criteria andPasswordLessThan(String value) {
addCriterion("`password` <", value, "password");
return (Criteria) this;
}
public Criteria andPasswordLessThanOrEqualTo(String value) {
addCriterion("`password` <=", value, "password");
return (Criteria) this;
}
public Criteria andPasswordLike(String value) {
addCriterion("`password` like", value, "password");
return (Criteria) this;
}
public Criteria andPasswordNotLike(String value) {
addCriterion("`password` not like", value, "password");
return (Criteria) this;
}
public Criteria andPasswordIn(List<String> values) {
addCriterion("`password` in", values, "password");
return (Criteria) this;
}
public Criteria andPasswordNotIn(List<String> values) {
addCriterion("`password` not in", values, "password");
return (Criteria) this;
}
public Criteria andPasswordBetween(String value1, String value2) {
addCriterion("`password` between", value1, value2, "password");
return (Criteria) this;
}
public Criteria andPasswordNotBetween(String value1, String value2) {
addCriterion("`password` not between", value1, value2, "password");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}
UserMapper.java
import org.apache.ibatis.annotations.Param;
public interface UserMapper {
long countByExample(UserExample example);
int deleteByExample(UserExample example);
int deleteByPrimaryKey(Long id);
int insert(User record);
int insertSelective(User record);
List<User> selectByExample(UserExample example);
User selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("record") User record, @Param("example") UserExample example);
int updateByExample(@Param("record") User record, @Param("example") UserExample example);
int updateByPrimaryKeySelective(User record);
int updateByPrimaryKey(User record);
}
generator-config.xml
<!--<table tableName="content"/>-->
<table tableName="user"/>
完成用户表基本增删改查功能
- 按照电子书管理,复制出一套用户管理的代码。
UserController.java
import com.li.response.CommonResp;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/list")
public CommonResp list(@Valid UserQueryReq req) {
CommonResp<PageResp<UserQueryResp>> resp = new CommonResp<>();
PageResp<UserQueryResp> list = userService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody UserSaveReq req) {
CommonResp resp = new CommonResp<>();
userService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
userService.delete(id);
return resp;
}
}
UserQueryReq.java
package com.li.request;
public class UserQueryReq extends PageReq {
private String loginName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public String toString() {
return "UserQueryReq{" +
"loginName='" + loginName + '\'' +
"} " + super.toString();
}
}
UserSaveReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class UserSaveReq {
private Long id;
@NotNull(message = "【用户名】不能为空")
private String loginName;
@NotNull(message = "【昵称】不能为空")
private String name;
@NotNull(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", message = "【密码】至少包含 数字和英文,长度6-20")
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@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(", loginName=").append(loginName);
sb.append(", name=").append(name);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
UserQueryResp.java
package com.li.response;
public class UserQueryResp {
private Long id;
private String loginName;
private String name;
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@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(", loginName=").append(loginName);
sb.append(", name=").append(name);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
UserService.java
@Service
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
@Resource
private UserMapper userMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<UserQueryResp> list(UserQueryReq req) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getLoginName())) {
criteria.andLoginNameEqualTo(req.getLoginName());
}
PageHelper.startPage(req.getPage(), req.getSize());
List<User> userList = userMapper.selectByExample(userExample);
PageInfo<User> pageInfo = new PageInfo<>(userList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<UserQueryResp> list = CopyUtil.copyList(userList, UserQueryResp.class);
PageResp<UserQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(UserSaveReq req) {
User user = CopyUtil.copy(req, User.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
user.setId(snowFlake.nextId());
userMapper.insert(user);
} else {
// 更新
userMapper.updateByPrimaryKey(user);
}
}
public void delete(Long id) {
userMapper.deleteByPrimaryKey(id);
}
}
the-header.vue
<a-menu-item key="/admin/user">
<router-link to="/admin/user">用户管理</router-link>
</a-menu-item>
index.ts
import AdminUser from '../views/admin/admin-user.vue'
{
path: '/admin/user',
name: 'AdminUser',
component: AdminUser
},
admin-user.vue
<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.loginName" 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="users"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
>
<template v-slot:action="{ text, record }">
<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>
<a-modal
title="用户表单"
v-model:visible="modalVisible"
:confirm-loading="modalLoading"
@ok="handleModalOk"
>
<a-form :model="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登陆名">
<a-input v-model:value="user.loginName" />
</a-form-item>
<a-form-item label="昵称">
<a-input v-model:value="user.name" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="user.password" />
</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: 'AdminUser',
setup() {
const param = ref();
param.value = {};
const users = ref();
const pagination = ref({
current: 1,
pageSize: 10,
total: 0
});
const loading = ref(false);
const columns = [
{
title: '登陆名',
dataIndex: 'loginName'
},
{
title: '名称',
dataIndex: 'name'
},
{
title: '密码',
dataIndex: 'password'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 数据查询
**/
const handleQuery = (params: any) => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
users.value = [];
axios.get("http://127.0.0.1:8080/user/list", {
params: {
page: params.page,
size: params.size,
loginName: param.value.loginName
}
}).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
users.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
});
};
// -------- 表单 ---------
const user = ref();
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
axios.post("http://127.0.0.1:8080/user/save", user.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;
user.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
user.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/user/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
param,
users,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
edit,
add,
user,
modalVisible,
modalLoading,
handleModalOk,
handleDelete
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
doc.vue
export default defineComponent({
/*name: 'AdminDoc',*/
name: 'Doc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
const defaultSelectedKeys = ref();
defaultSelectedKeys.value = [];
测试
用户名重复校验与自定义异常
增加校验用户名不能重复,增加自定义异常
BusinessException.java
package com.li.exception;
public class BusinessException extends RuntimeException{
private BusinessExceptionCode code;
public BusinessException (BusinessExceptionCode code) {
super(code.getDesc());
this.code = code;
}
public BusinessExceptionCode getCode() {
return code;
}
public void setCode(BusinessExceptionCode code) {
this.code = code;
}
/**
* 不写入堆栈信息,提高性能
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
}
BusinessExceptionCode.java
package com.li.exception;
public enum BusinessExceptionCode {
USER_LOGIN_NAME_EXIST("登录名已存在"),
LOGIN_USER_ERROR("用户名不存在或密码错误"),
VOTE_REPEAT("您已点赞过"),
;
private String desc;
BusinessExceptionCode(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
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;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommonResp validExceptionHandler(BusinessException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("业务异常:{}", e.getCode().getDesc());
commonResp.setSuccess(false);
commonResp.setMessage(e.getCode().getDesc());
return commonResp;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommonResp validExceptionHandler(Exception e) {
CommonResp commonResp = new CommonResp();
LOG.error("系统异常:", e);
commonResp.setSuccess(false);
commonResp.setMessage("系统出现异常,请联系管理员");
return commonResp;
}
}
修改UserService.java
@Service
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
@Resource
private UserMapper userMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<UserQueryResp> list(UserQueryReq req) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getLoginName())) {
criteria.andLoginNameEqualTo(req.getLoginName());
}
PageHelper.startPage(req.getPage(), req.getSize());
List<User> userList = userMapper.selectByExample(userExample);
PageInfo<User> pageInfo = new PageInfo<>(userList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<UserQueryResp> list = CopyUtil.copyList(userList, UserQueryResp.class);
PageResp<UserQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(UserSaveReq req) {
User user = CopyUtil.copy(req, User.class);
if (ObjectUtils.isEmpty(req.getId())) {
User userDB = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDB)) {
// 新增
user.setId(snowFlake.nextId());
userMapper.insert(user);
} else {
// 用户名已存在
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
}
} else {
// 更新
userMapper.updateByPrimaryKey(user);
}
}
public void delete(Long id) {
userMapper.deleteByPrimaryKey(id);
}
public User selectByLoginName(String LoginName) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andLoginNameEqualTo(LoginName);
List<User> userList = userMapper.selectByExample(userExample);
if (CollectionUtils.isEmpty(userList)) {
return null;
} else {
return userList.get(0);
}
}
}
修改的时候,用户名不允许修改
修改UserService.java
//userMapper.updateByPrimaryKey(user);
user.setLoginName(null);
userMapper.updateByPrimaryKeySelective(user);
}
修改admin-user.vue
<a-form :model="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登陆名">
<!-- <a-input v-model:value="user.loginName" />-->
<a-input v-model:value="user.loginName" :disabled="!!user.id"/>
</a-form-item>
关于密码的两层加密处理
密码加密传输和加密存储
修改UserController.java
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody UserSaveReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.save(req);
return resp;
修改UserSaveReq.java
@NotNull(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
/* @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$", message = "【密码】至少包含 数字和英文,长度6-20")*/
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32")
private String password;
新增index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="<%= BASE_URL %>js/md5.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
新增md5.js
var KEY = "!@#QWERT";
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hexMd5(s) {
return hex_md5(s);
}
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }
/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test()
{
return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length
*/
function core_md5(x, len)
{
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for(var i = 0; i < x.length; i += 16)
{
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t)
{
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
/*
* Calculate the HMAC-MD5, of a key and some data
*/
function core_hmac_md5(key, data)
{
var bkey = str2binl(key);
if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
return core_md5(opad.concat(hash), 512 + 128);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}
/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
function str2binl(str)
{
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
return bin;
}
/*
* Convert an array of little-endian words to a string
*/
function binl2str(bin)
{
var str = "";
var mask = (1 << chrsz) - 1;
for(var i = 0; i < bin.length * 32; i += chrsz)
str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
return str;
}
/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray)
{
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++)
{
str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
}
return str;
}
/*
* Convert an array of little-endian words to a base-64 string
*/
function binl2b64(binarray)
{
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
for(var i = 0; i < binarray.length * 4; i += 3)
{
var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16)
| (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
| ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
for(var j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
}
}
return str;
}
修改admin-user.vue
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'AdminUser',
setup() {
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
user.value.password = hexMd5(user.value.password + KEY);
axios.post("/user/save", user.value).then((response) => {
modalLoading.value = false;
const data = response.data; // data = commonResp
增加重置密码功能
修改的时候,不修改密码
修改UserService.java
// 更新
user.setLoginName(null);
user.setPassword(null);
userMapper.updateByPrimaryKeySelective(user);
修改admin-user.vue
</a-form-item>
<!-- <a-form-item label="密码">
<a-input v-model:value="user.password" />-->
<a-form-item label="密码" v-show="!user.id">
<a-input v-model:value="user.password"/>
</a-form-item>
增加单独的重置密码功能
resetPassword - UserController.java
@PostMapping("/reset-password")
public CommonResp resetPassword(@Valid @RequestBody UserResetPasswordReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.resetPassword(req);
return resp;
}
新增UserResetPasswordReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class UserResetPasswordReq {
private Long id;
@NotNull(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】至少包含 数字和英文,长度6-32")
private String password;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@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(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
resetPassword - UserService.java
/**
* 修改密码
*/
public void resetPassword(UserResetPasswordReq req) {
User user = CopyUtil.copy(req, User.class);
userMapper.updateByPrimaryKeySelective(user);
}
修改admin-user.vue
<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.loginName" 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="users"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
>
<template v-slot:action="{ text, record }">
<a-space size="small">
<a-button type="primary" @click="resetPassword(record)">
重置密码
</a-button>
<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="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登陆名">
<a-input v-model:value="user.loginName" :disabled="!!user.id"/>
</a-form-item>
<a-form-item label="昵称">
<a-input v-model:value="user.name" />
</a-form-item>
<a-form-item label="密码" v-show="!user.id">
<a-input v-model:value="user.password"/>
</a-form-item>
</a-form>
</a-modal>
<a-modal
title="重置密码"
v-model:visible="resetModalVisible"
:confirm-loading="resetModalLoading"
@ok="handleResetModalOk"
>
<a-form :model="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="新密码">
<a-input v-model:value="user.password"/>
</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";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'AdminUser',
setup() {
const param = ref();
param.value = {};
const users = ref();
const pagination = ref({
current: 1,
pageSize: 10,
total: 0
});
const loading = ref(false);
const columns = [
{
title: '登陆名',
dataIndex: 'loginName'
},
{
title: '名称',
dataIndex: 'name'
},
{
title: '密码',
dataIndex: 'password'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
];
/**
* 数据查询
**/
const handleQuery = (params: any) => {
loading.value = true;
// 如果不清空现有数据,则编辑保存重新加载数据后,再点编辑,则列表显示的还是编辑前的数据
users.value = [];
axios.get("http://127.0.0.1:8080/user/list", {
params: {
page: params.page,
size: params.size,
loginName: param.value.loginName
}
}).then((response) => {
loading.value = false;
const data = response.data;
if (data.success) {
users.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
});
};
// -------- 表单 ---------
const user = ref();
const modalVisible = ref(false);
const modalLoading = ref(false);
const handleModalOk = () => {
modalLoading.value = true;
user.value.password = hexMd5(user.value.password + KEY);
axios.post("http://127.0.0.1:8080/user/save", user.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;
user.value = Tool.copy(record);
};
/**
* 新增
*/
const add = () => {
modalVisible.value = true;
user.value = {};
};
const handleDelete = (id: number) => {
axios.delete("http://127.0.0.1:8080/user/delete/" + id).then((response) => {
const data = response.data; // data = commonResp
if (data.success) {
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
});
};
// -------- 重置密码 ---------
const resetModalVisible = ref(false);
const resetModalLoading = ref(false);
const handleResetModalOk = () => {
resetModalLoading.value = true;
user.value.password = hexMd5(user.value.password + KEY);
axios.post("http://127.0.0.1:8080/user/reset-password", user.value).then((response) => {
resetModalLoading.value = false;
const data = response.data; // data = commonResp
if (data.success) {
resetModalVisible.value = false;
// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
} else {
message.error(data.message);
}
});
};
/**
* 重置密码
*/
const resetPassword = (record: any) => {
resetModalVisible.value = true;
user.value = Tool.copy(record);
user.value.password = null;
};
onMounted(() => {
handleQuery({
page: 1,
size: pagination.value.pageSize,
});
});
return {
param,
users,
pagination,
columns,
loading,
handleTableChange,
handleQuery,
edit,
add,
user,
modalVisible,
modalLoading,
handleModalOk,
handleDelete,
resetModalVisible,
resetModalLoading,
handleResetModalOk,
resetPassword
}
}
});
</script>
<style scoped>
img {
width: 50px;
height: 50px;
}
</style>
单点登录token与JWT介绍
登录
- 前端输入用户名密码
- 校验用户名密码
- 生成token
- 后端保存token (redis)
- 前端保存token
校验
- 前端请求时,带上token(放在header)
- 登录拦截器,校验token(到redis获取token)
- 校验成功则继续后面的业务
- 校验失败则回到登录页面
单点登录系统
- 淘宝 支付宝
- A B C...
- 用户管理、登录、登录校验、退出登录
token与JWT
- token+redis:token是无意义的
- JWT:token是有意义的,加密的,包含业务信息,一般是用户信息,可被破解出来
- 登录标识:就是令牌,就是token,就是—串唯—的字符串
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
JwtUtil.sign
JwtUtil.verity
登录功能开发
后端增加登录接口
UserController.java
@PostMapping("/login")
public CommonResp login(@Valid @RequestBody UserLoginReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp<UserLoginResp> resp = new CommonResp<>();
UserLoginResp userLoginResp = userService.login(req);
resp.setContent(userLoginResp);
return resp;
}
BusinessExceptionCode.java
USER_LOGIN_NAME_EXIST("登录名已存在"),
LOGIN_USER_ERROR("用户名不存在或密码错误"),
UserLoginReq.java
package com.li.request;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class UserLoginReq {
@NotNull(message = "【用户名】不能为空")
private String loginName;
@NotNull(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确")
private String password;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", loginName=").append(loginName);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
UserLoginResp.java
package com.li.response;
public class UserLoginResp {
private Long id;
private String loginName;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@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(", loginName=").append(loginName);
sb.append(", name=").append(name);
sb.append("]");
return sb.toString();
}
}
UserService.java
/**
* 登录
*/
public UserLoginResp login(UserLoginReq req) {
User userDb = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDb)) {
// 用户名不存在
LOG.info("用户名不存在, {}", req.getLoginName());
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
} else {
if (userDb.getPassword().equals(req.getPassword())) {
// 登录成功
UserLoginResp userLoginResp = CopyUtil.copy(userDb, UserLoginResp.class);
return userLoginResp;
} else {
// 密码不对
LOG.info("密码不对, 输入密码:{}, 数据库密码:{}", req.getPassword(), userDb.getPassword());
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
}
}
}
前端增加登录框
修改the-header.vue
<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/user">
<router-link to="/admin/user">用户管理</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="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a class="login-menu" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: 'the-header',
setup () {
const loginUser = ref({
loginName: "test",
password: "test"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录")
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
}
</style>
前后端调试成功,完成基本的登录功能
修改UserLoginReq.java
package com.li.request;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
public class UserLoginReq {
@NotEmpty(message = "【用户名】不能为空")
private String loginName;
@NotEmpty(message = "【密码】不能为空")
// @Length(min = 6, max = 20, message = "【密码】6~20位")
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密码】规则不正确")
private String password;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", loginName=").append(loginName);
sb.append(", password=").append(password);
sb.append("]");
return sb.toString();
}
}
修改UserService.java
@Service
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
@Resource
private UserMapper userMapper;
@Resource
private SnowFlake snowFlake;
public PageResp<UserQueryResp> list(UserQueryReq req) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
if (!ObjectUtils.isEmpty(req.getLoginName())) {
criteria.andLoginNameEqualTo(req.getLoginName());
}
PageHelper.startPage(req.getPage(), req.getSize());
List<User> userList = userMapper.selectByExample(userExample);
PageInfo<User> pageInfo = new PageInfo<>(userList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<UserQueryResp> list = CopyUtil.copyList(userList, UserQueryResp.class);
PageResp<UserQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(UserSaveReq req) {
User user = CopyUtil.copy(req, User.class);
if (ObjectUtils.isEmpty(req.getId())) {
User userDB = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDB)) {
// 新增
user.setId(snowFlake.nextId());
userMapper.insert(user);
} else {
// 用户名已存在
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
}
} else {
// 更新
user.setLoginName(null);
user.setPassword(null);
userMapper.updateByPrimaryKeySelective(user);
}
}
public void delete(Long id) {
userMapper.deleteByPrimaryKey(id);
}
public User selectByLoginName(String LoginName) {
UserExample userExample = new UserExample();
UserExample.Criteria criteria = userExample.createCriteria();
criteria.andLoginNameEqualTo(LoginName);
List<User> userList = userMapper.selectByExample(userExample);
if (CollectionUtils.isEmpty(userList)) {
return null;
} else {
return userList.get(0);
}
}
/**
* 修改密码
*/
public void resetPassword(UserResetPasswordReq req) {
User user = CopyUtil.copy(req, User.class);
userMapper.updateByPrimaryKeySelective(user);
}
/**
* 登录
*/
public UserLoginResp login(UserLoginReq req) {
User userDb = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDb)) {
// 用户名不存在
LOG.info("用户名不存在, {}", req.getLoginName());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
} else {
if (userDb.getPassword().equals(req.getPassword())) {
// 登录成功
UserLoginResp userLoginResp = CopyUtil.copy(userDb, UserLoginResp.class);
return userLoginResp;
} else {
// 密码不对
LOG.info("密码不对, 输入密码:{}, 数据库密码:{}", req.getPassword(), userDb.getPassword());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
}
}
}
}
修改the-header.vue
<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/user">
<router-link to="/admin/user">用户管理</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="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a class="login-menu" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
const loginUser = ref({
loginName: "test",
password: "test"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('http://127.0.0.1:8080/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
}
</style>
登录成功处理并集成vuex
集成redis,登录成功后将登录信息放入redis中
pom.xml
<!--整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
@Resource
private UserService userService;
@Resource
private SnowFlake snowFlake;
@Resource
private RedisTemplate redisTemplate;
@GetMapping("/list")
public CommonResp list(@Valid UserQueryReq req) {
CommonResp<PageResp<UserQueryResp>> resp = new CommonResp<>();
PageResp<UserQueryResp> list = userService.list(req);
resp.setContent(list);
return resp;
}
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody UserSaveReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.save(req);
return resp;
}
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id) {
CommonResp resp = new CommonResp<>();
userService.delete(id);
return resp;
}
@PostMapping("/reset-password")
public CommonResp resetPassword(@Valid @RequestBody UserResetPasswordReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp resp = new CommonResp<>();
userService.resetPassword(req);
return resp;
}
@PostMapping("/login")
public CommonResp login(@Valid @RequestBody UserLoginReq req) {
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
CommonResp<UserLoginResp> resp = new CommonResp<>();
UserLoginResp userLoginResp = userService.login(req);
Long token = snowFlake.nextId();
LOG.info("生成单点登录token:{},并放入redis中", token);
userLoginResp.setToken(token.toString());
redisTemplate.opsForValue().set(token, JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
resp.setContent(userLoginResp);
return resp;
}
}
修改UserLoginResp.java
package com.li.response;
public class UserLoginResp {
private Long id;
private String loginName;
private String name;
private String token;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public String toString() {
return "UserLoginResp{" +
"id=" + id +
", loginName='" + loginName + '\'' +
", name='" + name + '\'' +
", token='" + token + '\'' +
'}';
}
}
redis配置 - application.yml
# redis配置
redis:
host:localhost
port:6379
前端显示登录昵称,遗留问题,刷新会没有
修改the-header.vue
<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/user">
<router-link to="/admin/user">用户管理</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="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a class="login-menu" v-show="user.id">
<span>您好:{{user.name}}</span>
</a>
<a class="login-menu" v-show="!user.id" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
// 登录后保存
const user = ref();
user.value = {};
// 用来登录
const loginUser = ref({
loginName: "test",
password: "test123"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('http://127.0.0.1:8080/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
user.value = data.content;
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login,
user
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
}
</style>
使用vuex保存用户信息,方便其它组件获取登录信息。遗留问题,刷新后值没有了
修改the-footer.vue
<template>
<a-layout-footer style="text-align: center">
电子书,欢迎:{{user.name}}
</a-layout-footer>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import store from "@/store";
export default defineComponent({
name: 'the-footer',
setup() {
const user = computed(() => store.state.user);
return {
user
}
}
});
</script>
新增/store/index.ts
import { createStore } from 'vuex'
declare let SessionStorage: any;
const USER = "USER";
const store = createStore({
state: {
user: SessionStorage.get(USER) || {}
},
mutations: {
setUser (state, user) {
console.log("store user:", user);
state.user = user;
SessionStorage.set(USER, user);
}
},
actions: {
},
modules: {
}
});
export default store;
vuex整合sessionStorage,解决刷新数据丢失问题
修改index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="<%= BASE_URL %>js/md5.js"></script>
<script src="<%= BASE_URL %>js/session-storage.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
新增/js/session-storage.js
SessionStorage = {
get: function (key) {
var v = sessionStorage.getItem(key);
if (v && typeof(v) !== "undefined" && v !== "undefined") {
return JSON.parse(v);
}
},
set: function (key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
},
remove: function (key) {
sessionStorage.removeItem(key);
},
clearAll: function () {
sessionStorage.clear();
}
};
修改the-footer.vue
<template>
<a-layout-footer style="text-align: center">
电子书<span v-show="user.id">,欢迎:{{user.name}}</span>
</a-layout-footer>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import store from "@/store";
export default defineComponent({
name: 'the-footer',
setup() {
const user = computed(() => store.state.user);
return {
user
}
}
});
</script>
修改the-header.vue
<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/user">
<router-link to="/admin/user">用户管理</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="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a class="login-menu" v-show="user.id">
<span>您好:{{user.name}}</span>
</a>
<a class="login-menu" v-show="!user.id" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import store from "@/store";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
// 登录后保存
const user = computed(() => store.state.user);
// 用来登录
const loginUser = ref({
loginName: "test",
password: "test123"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('http://127.0.0.1:8080/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
store.commit("setUser", user.value);
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login,
user
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
}
</style>
增加退出登录功能
增加退出登录功能,退出登录时,前端清除store登录信息,后端清除redis登录信息
logout - UserController.java
@GetMapping("/logout/{token}")
public CommonResp logout(@PathVariable String token) {
CommonResp resp = new CommonResp<>();
redisTemplate.delete(token);
LOG.info("从redis中删除token: {}", token);
return resp;
}
修改the-header.vue
<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/user">
<router-link to="/admin/user">用户管理</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="/admin/category">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a-popconfirm
title="确认退出登录?"
ok-text="是"
cancel-text="否"
@confirm="logout()"
>
<a class="login-menu" v-show="user.id">
<span>退出登录</span>
</a>
</a-popconfirm>
<a class="login-menu" v-show="user.id">
<span>您好:{{user.name}}</span>
</a>
<a class="login-menu" v-show="!user.id" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import store from "@/store";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
// 登录后保存
const user = computed(() => store.state.user);
// 用来登录
const loginUser = ref({
loginName: "test",
password: "test123"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('http://127.0.0.1:8080/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
store.commit("setUser", data.content);
} else {
message.error(data.message);
}
});
};
// 退出登录
const logout = () => {
console.log("退出登录开始");
axios.get('http://127.0.0.1:8080/user/logout/' + user.value.token).then((response) => {
const data = response.data;
if (data.success) {
message.success("退出登录成功!");
store.commit("setUser", {});
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login,
user,
logout
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
padding-left: 10px;
}
</style>
前后端增加登录拦截
SpringMvcConfig.java
package com.li.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/find-content/**"
);
}
}
修改UserController.java
//redisTemplate.opsForValue().set(token, JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(token.toString(), JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
修改LoginInterceptor.java
package com.li.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
@Resource
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}
String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);
//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info( "token为空,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn( "token无效,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}
修改main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';
import * as Icons from '@ant-design/icons-vue';
import axios from 'axios';
import {Tool} from "@/util/tool";
axios.defaults.baseURL = process.env.VUE_APP_SERVER;
/**
* axios拦截器
*/
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
const token = store.state.user.token;
if (Tool.isNotEmpty(token)) {
config.headers.token = token;
console.log("请求headers增加token:", token);
}
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
console.log('返回错误:', error);
return Promise.reject(error);
});
const app = createApp(App);
app.use(store).use(router).use(Antd).mount('#app');
// 全局使用图标
const icons: any = Icons;
for (const i in icons) {
app.component(i, icons[i]);
}
console.log('环境:', process.env.NODE_ENV);
console.log('服务端:', process.env.VUE_APP_SERVER);
前端未登录时,隐藏菜单
修改the-header.vue
<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/user" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/user">用户管理</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="/admin/category" :style="user.id? {} : {display:'none'}">
<router-link to="/admin/category">分类管理</router-link>
</a-menu-item>
<a-menu-item key="/about">
<router-link to="/about">关于我们</router-link>
</a-menu-item>
<a-popconfirm
title="确认退出登录?"
ok-text="是"
cancel-text="否"
@confirm="logout()"
>
<a class="login-menu" v-show="user.id">
<span>退出登录</span>
</a>
</a-popconfirm>
<a class="login-menu" v-show="user.id">
<span>您好:{{user.name}}</span>
</a>
<a class="login-menu" v-show="!user.id" @click="showLoginModal">
<span>登录</span>
</a>
</a-menu>
<a-modal
title="登录"
v-model:visible="loginModalVisible"
:confirm-loading="loginModalLoading"
@ok="login"
>
<a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="登录名">
<a-input v-model:value="loginUser.loginName" />
</a-form-item>
<a-form-item label="密码">
<a-input v-model:value="loginUser.password" type="password" />
</a-form-item>
</a-form>
</a-modal>
</a-layout-header>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import store from "@/store";
declare let hexMd5: any;
declare let KEY: any;
export default defineComponent({
name: 'the-header',
setup () {
// 登录后保存
const user = computed(() => store.state.user);
// 用来登录
const loginUser = ref({
loginName: "test",
password: "test123"
});
const loginModalVisible = ref(false);
const loginModalLoading = ref(false);
const showLoginModal = () => {
loginModalVisible.value = true;
};
// 登录
const login = () => {
console.log("开始登录");
loginModalLoading.value = true;
loginUser.value.password = hexMd5(loginUser.value.password + KEY);
axios.post('/user/login', loginUser.value).then((response) => {
loginModalLoading.value = false;
const data = response.data;
if (data.success) {
loginModalVisible.value = false;
message.success("登录成功!");
store.commit("setUser", data.content);
} else {
message.error(data.message);
}
});
};
// 退出登录
const logout = () => {
console.log("退出登录开始");
axios.get('/user/logout/' + user.value.token).then((response) => {
const data = response.data;
if (data.success) {
message.success("退出登录成功!");
store.commit("setUser", {});
} else {
message.error(data.message);
}
});
};
return {
loginModalVisible,
loginModalLoading,
showLoginModal,
loginUser,
login,
user,
logout
}
}
});
</script>
<style>
.login-menu {
float: right;
color: white;
padding-left: 10px;
}
</style>
增加路由登录拦截,未登录时跳到首页
修改index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/home.vue'
import About from '../views/about.vue'
import Doc from '../views/doc.vue'
import AdminUser from '../views/admin/admin-user.vue'
import AdminEbook from '../views/admin/admin-ebook.vue'
import AdminCategory from '../views/admin/admin-category.vue'
import AdminDoc from '../views/admin/admin-doc.vue'
import store from "@/store";
import {Tool} from "@/util/tool";
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/doc',
name: 'Doc',
component: Doc
},
{
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')
},
{
path: '/admin/user',
name: 'AdminUser',
component: AdminUser,
meta: {
loginRequire: true
}
},
{
path: '/admin/ebook',
name: 'AdminEbook',
component: AdminEbook,
meta: {
loginRequire: true
}
},
{
path: '/admin/category',
name: 'AdminCategory',
component: AdminCategory,
meta: {
loginRequire: true
}
},
{
path: '/admin/doc',
name: 'AdminDoc',
component: AdminDoc,
meta: {
loginRequire: true
}
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.loginRequire属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否需要登录校验:", item.meta.loginRequire);
return item.meta.loginRequire
})) {
const loginUser = store.state.user;
if (Tool.isEmpty(loginUser)) {
console.log("用户未登录!");
next('/');
} else {
next();
}
} else {
next();
}
});
export default router
十五、阅读量&点赞量
文档阅读数更新
阅读文档时,文档阅读数+1
自定义SQL - 新建DocMapperCust.java
package com.li.mapper;
import org.apache.ibatis.annotations.Param;
public interface DocMapperCust {
public void increaseViewCount(@Param("id") Long id);
}
新建DocMapperCust.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.li.mapper.DocMapperCust" >
<update id="increaseViewCount">
update doc set view_count = view_count + 1 where id = #{id}
</update>
</mapper>
修改DocService.java
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private DocMapperCust docMapperCust;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
// 文档阅读数+1
docMapperCust.increaseViewCount(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
}
文档页面显示文档阅读数点赞数
修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<h3 v-if="level1.length === 0">对不起,找不到相关文档!</h3>
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
:defaultSelectedKeys="defaultSelectedKeys"
>
</a-tree>
</a-col>
<a-col :span="18">
<div>
<h2>{{doc.name}}</h2>
<div>
<span>阅读数:{{doc.viewCount}}</span>
<span>点赞数:{{doc.voteCount}}</span>
</div>
<a-divider style="height: 2px; background-color: #9999cc"/>
</div>
<div class="wangeditor" :innerHTML="html"></div>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'Doc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
const defaultSelectedKeys = ref();
defaultSelectedKeys.value = [];
// 当前选中的文档
const doc = ref();
doc.value = {};
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 内容查询
**/
const handleQueryContent = (id: number) => {
axios.get("/doc/find-content/" + id).then((response) => {
const data = response.data;
if (data.success) {
html.value = data.content;
} else {
message.error(data.message);
}
});
};
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
if (Tool.isNotEmpty(level1)) {
defaultSelectedKeys.value = [level1.value[0].id];
handleQueryContent(level1.value[0].id);
// 初始显示文档信息
doc.value = level1.value[0];
}
} else {
message.error(data.message);
}
});
};
const onSelect = (selectedKeys: any, info: any) => {
console.log('selected', selectedKeys, info);
if (Tool.isNotEmpty(selectedKeys)) {
// 选中某一节点时,加载该节点的文档信息
doc.value = info.selectedNodes[0].props;
// 加载内容
handleQueryContent(selectedKeys[0]);
}
};
onMounted(() => {
handleQuery();
});
return {
level1,
html,
onSelect,
defaultSelectedKeys,
doc
}
}
});
</script>
<style>
/* wangeditor默认样式, 参照: http://www.wangeditor.com/doc/pages/02-%E5%86%85%E5%AE%B9%E5%A4%84%E7%90%86/03-%E8%8E%B7%E5%8F%96html.html */
/* table 样式 */
.wangeditor table {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.wangeditor table td,
.wangeditor table th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 3px 5px;
}
.wangeditor table th {
border-bottom: 2px solid #ccc;
text-align: center;
}
/* blockquote 样式 */
.wangeditor blockquote {
display: block;
border-left: 8px solid #d0e5f2;
padding: 5px 10px;
margin: 10px 0;
line-height: 1.4;
font-size: 100%;
background-color: #f1f1f1;
}
/* code 样式 */
.wangeditor code {
display: inline-block;
*display: inline;
*zoom: 1;
background-color: #f1f1f1;
border-radius: 3px;
padding: 3px 5px;
margin: 0 3px;
}
.wangeditor pre code {
display: block;
}
/* ul ol 样式 */
.wangeditor ul, ol {
margin: 10px 0 10px 20px;
}
/* 和antdv p冲突,覆盖掉 */
.wangeditor blockquote p {
font-family:"YouYuan";
margin: 20px 10px !important;
font-size: 16px !important;
font-weight:600;
}
</style>
文档点赞功能开发
增加文档点赞功能
DocController.java
@GetMapping("/vote/{id}")
public CommonResp vote(@PathVariable Long id) {
CommonResp commonResp = new CommonResp();
docService.vote(id);
return commonResp;
}
DocMapperCust.java
import org.apache.ibatis.annotations.Param;
public interface DocMapperCust {
public void increaseViewCount(@Param("id") Long id);
public void increaseVoteCount(@Param("id") Long id);
}
DocMapperCust.xml
<update id="increaseVoteCount">
update doc set vote_count = vote_count + 1 where id = #{id}
</update>
DocService.java
/**
* 点赞
*/
public void vote(Long id) {
docMapperCust.increaseVoteCount(id);
}
点赞 - 修改doc.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<h3 v-if="level1.length === 0">对不起,找不到相关文档!</h3>
<a-row>
<a-col :span="6">
<a-tree
v-if="level1.length > 0"
:tree-data="level1"
@select="onSelect"
:replaceFields="{title: 'name', key: 'id', value: 'id'}"
:defaultExpandAll="true"
:defaultSelectedKeys="defaultSelectedKeys"
>
</a-tree>
</a-col>
<a-col :span="18">
<div>
<h2>{{doc.name}}</h2>
<div>
<span>阅读数:{{doc.viewCount}}</span>
<span>点赞数:{{doc.voteCount}}</span>
</div>
<a-divider style="height: 2px; background-color: #9999cc"/>
</div>
<div class="wangeditor" :innerHTML="html"></div>
<div class="vote-div">
<a-button type="primary" shape="round" :size="'large'" @click="vote">
<template #icon><LikeOutlined /> 点赞:{{doc.voteCount}} </template>
</a-button>
</div>
</a-col>
</a-row>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, createVNode } from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";
import {useRoute} from "vue-router";
export default defineComponent({
name: 'Doc',
setup() {
const route = useRoute();
const docs = ref();
const html = ref();
const defaultSelectedKeys = ref();
defaultSelectedKeys.value = [];
// 当前选中的文档
const doc = ref();
doc.value = {};
/**
* 一级文档树,children属性就是二级文档
* [{
* id: "",
* name: "",
* children: [{
* id: "",
* name: "",
* }]
* }]
*/
const level1 = ref(); // 一级文档树,children属性就是二级文档
level1.value = [];
/**
* 内容查询
**/
const handleQueryContent = (id: number) => {
axios.get("/doc/find-content/" + id).then((response) => {
const data = response.data;
if (data.success) {
html.value = data.content;
} else {
message.error(data.message);
}
});
};
/**
* 数据查询
**/
const handleQuery = () => {
axios.get("/doc/all/" + route.query.ebookId).then((response) => {
const data = response.data;
if (data.success) {
docs.value = data.content;
level1.value = [];
level1.value = Tool.array2Tree(docs.value, 0);
if (Tool.isNotEmpty(level1)) {
defaultSelectedKeys.value = [level1.value[0].id];
handleQueryContent(level1.value[0].id);
// 初始显示文档信息
doc.value = level1.value[0];
}
} else {
message.error(data.message);
}
});
};
const onSelect = (selectedKeys: any, info: any) => {
console.log('selected', selectedKeys, info);
if (Tool.isNotEmpty(selectedKeys)) {
// 选中某一节点时,加载该节点的文档信息
doc.value = info.selectedNodes[0].props;
// 加载内容
handleQueryContent(selectedKeys[0]);
}
};
// 点赞
const vote = () => {
axios.get('/doc/vote/' + doc.value.id).then((response) => {
const data = response.data;
if (data.success) {
doc.value.voteCount++;
} else {
message.error(data.message);
}
});
};
onMounted(() => {
handleQuery();
});
return {
level1,
html,
onSelect,
defaultSelectedKeys,
doc,
vote
}
}
});
</script>
<style>
/* wangeditor默认样式, 参照: http://www.wangeditor.com/doc/pages/02-%E5%86%85%E5%AE%B9%E5%A4%84%E7%90%86/03-%E8%8E%B7%E5%8F%96html.html */
/* table 样式 */
.wangeditor table {
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
}
.wangeditor table td,
.wangeditor table th {
border-bottom: 1px solid #ccc;
border-right: 1px solid #ccc;
padding: 3px 5px;
}
.wangeditor table th {
border-bottom: 2px solid #ccc;
text-align: center;
}
/* blockquote 样式 */
.wangeditor blockquote {
display: block;
border-left: 8px solid #d0e5f2;
padding: 5px 10px;
margin: 10px 0;
line-height: 1.4;
font-size: 100%;
background-color: #f1f1f1;
}
/* code 样式 */
.wangeditor code {
display: inline-block;
*display: inline;
*zoom: 1;
background-color: #f1f1f1;
border-radius: 3px;
padding: 3px 5px;
margin: 0 3px;
}
.wangeditor pre code {
display: block;
}
/* ul ol 样式 */
.wangeditor ul, ol {
margin: 10px 0 10px 20px;
}
/* 和antdv p冲突,覆盖掉 */
.wangeditor blockquote p {
font-family:"YouYuan";
margin: 20px 10px !important;
font-size: 16px !important;
font-weight:600;
}
/* 点赞 */
.vote-div {
padding: 15px;
text-align: center;
}
</style>
同一个IP,一天只能对一个文档点赞一次
修改LogAspect.java
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.li.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
新增RequestContext.java
import java.io.Serializable;
public class RequestContext implements Serializable {
private static ThreadLocal<String> remoteAddr = new ThreadLocal<>();
public static String getRemoteAddr() {
return remoteAddr.get();
}
public static void setRemoteAddr(String remoteAddr) {
RequestContext.remoteAddr.set(remoteAddr);
}
}
修改BusinessExceptionCode.java
LOGIN_USER_ERROR("用户名不存在或密码错误"),
VOTE_REPEAT("您已点赞过"),
修改DocService.java
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private DocMapperCust docMapperCust;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
@Resource
public RedisUtil redisUtil;
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
// 文档阅读数+1
docMapperCust.increaseViewCount(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 3600 * 24)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
}
}
新增RedisUtil.java
package com.li.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);
@Resource
private RedisTemplate redisTemplate;
/**
* true:不存在,放一个KEY
* false:已存在
* @param key
* @param second
* @return
*/
public boolean validateRepeat(String key, long second) {
if (redisTemplate.hasKey(key)) {
LOG.info("key已存在:{}", key);
return false;
} else {
LOG.info("key不存在,放入:{},过期 {} 秒", key, second);
redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS);
return true;
}
}
}
电子书信息更新方案调研
SpringBoot定时任务示例
修改WikiApplication.java
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
public class WikiApplication {
新增job/TestJob.java
package com.li.job;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class TestJob {
private static final Logger LOG = LoggerFactory.getLogger(TestJob.class);
/**
* 固定时间间隔,fixedRate单位毫秒
*/
@Scheduled(fixedRate = 1000)
public void simple() throws InterruptedException {
SimpleDateFormat formatter = new SimpleDateFormat("mm:ss");
String dateString = formatter.format(new Date());
Thread.sleep(2000);
LOG.info("每隔5秒钟执行一次: {}", dateString);
}
/**
* 自定义cron表达式跑批
* 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
*/
@Scheduled(cron = "*/1 * * * * ?")
public void cron() throws InterruptedException {
SimpleDateFormat formatter = new SimpleDateFormat("mm:ss SSS");
String dateString = formatter.format(new Date());
Thread.sleep(1500);
LOG.info("每隔1秒钟执行一次: {}", dateString);
}
}
完成电子书信息定时更新功能
增加定时器,定时执行电子书信息更新SQL
新增DocJob.java
@Component
public class DocJob {
private static final Logger LOG = LoggerFactory.getLogger(DocJob.class);
@Resource
private DocService docService;
/**
* 每30秒更新电子书信息
*/
@Scheduled(cron = "5/30 * * * * ?")
public void cron() {
docService.updateEbookInfo();
}
}
修改DocMapperCust.java
public void updateEbookInfo();
修改DocMapperCust.xml
<update id="updateEbookInfo">
update ebook t1, (select ebook_id, count(1) doc_count, sum(view_count) view_count, sum(vote_count) vote_count from doc group by ebook_id) t2
set t1.doc_count = t2.doc_count, t1.view_count = t2.view_count, t1.vote_count = t2.vote_count
where t1.id = t2.ebook_id
</update>
修改DocService.java
public void updateEbookInfo() {
docMapperCust.updateEbookInfo();
}
增加定时器,定时执行电子书信息更新SQL,增加日志
修改DocJob.java
@Component
public class DocJob {
private static final Logger LOG = LoggerFactory.getLogger(DocJob.class);
@Resource
private DocService docService;
/**
* 每30秒更新电子书信息
*/
@Scheduled(cron = "5/30 * * * * ?")
public void cron() {
LOG.info("更新电子书下的文档数据开始");
long start = System.currentTimeMillis();
docService.updateEbookInfo();
LOG.info("更新电子书下的文档数据结束,耗时:{}毫秒", System.currentTimeMillis() - start);
}
}
首页显示电子书信息:文档数、阅读数、点赞数
修改SpringMvcConfig.java
"/doc/all/**",
"/doc/vote/**",
修改home.vue
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
@click="handleClick"
>
<a-menu-item key="welcome">
<MailOutlined />
<span>欢迎</span>
</a-menu-item>
<a-sub-menu v-for="item in level1" :key="item.id">
<template v-slot:title>
<span><user-outlined />{{item.name}}</span>
</template>
<a-menu-item v-for="child in item.children" :key="child.id">
<MailOutlined /><span>{{child.name}}</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<div class="welcome" v-show="isShowWelcome">
<h1>欢迎使用知识库</h1>
</div>
<a-list v-show="!isShowWelcome" item-layout="vertical" size="large" :grid="{ gutter: 20, column: 3 }" :data-source="ebooks">
<template #renderItem="{ item }">
<a-list-item key="item.name">
<template #actions>
<span>
<component v-bind:is="'FileOutlined'" style="margin-right: 8px" />
{{ item.docCount }}
</span>
<span>
<component v-bind:is="'UserOutlined'" style="margin-right: 8px" />
{{ item.viewCount }}
</span>
<span>
<component v-bind:is="'LikeOutlined'" style="margin-right: 8px" />
{{ item.voteCount }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<router-link :to="'/doc?ebookId=' + item.id">
{{ item.name }}
</router-link>
</template>
<template #avatar><a-avatar :src="item.cover"/></template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, reactive, toRef } from 'vue';
import axios from 'axios';
import { message } from 'ant-design-vue';
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'Home',
setup() {
const ebooks = ref();
// const ebooks1 = reactive({books: []});
const level1 = ref();
let categorys: any;
/**
* 查询所有分类
**/
const handleQueryCategory = () => {
axios.get("/category/all").then((response) => {
const data = response.data;
if (data.success) {
categorys = data.content;
console.log("原始数组:", categorys);
level1.value = [];
level1.value = Tool.array2Tree(categorys, 0);
console.log("树形结构:", level1.value);
} else {
message.error(data.message);
}
});
};
const isShowWelcome = ref(true);
let categoryId2 = 0;
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;
});
};
const handleClick = (value: any) => {
// console.log("menu click", value)
if (value.key === 'welcome') {
isShowWelcome.value = true;
} else {
categoryId2 = value.key;
isShowWelcome.value = false;
handleQueryEbook();
}
// isShowWelcome.value = value.key === 'welcome';
};
onMounted(() => {
handleQueryCategory();
// handleQueryEbook();
});
return {
ebooks,
// ebooks2: toRef(ebooks1, "books"),
// listData,
pagination: {
onChange: (page: any) => {
console.log(page);
},
pageSize: 3,
},
// actions: [
// { type: 'StarOutlined', text: '156' },
// { type: 'LikeOutlined', text: '156' },
// { type: 'MessageOutlined', text: '2' },
// ],
handleClick,
level1,
isShowWelcome
}
}
});
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
日志流水号的使用
- logback增加自定义参数。
增加日志流水号,方便生产运维
修改LogAspect.java
@Resource
private SnowFlake snowFlake;
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
修改DocJob.java
@Resource
private SnowFlake snowFlake;
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
新增resourceslogback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 修改一下路径-->
<property name="PATH" value="./log"></property>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
<Pattern>%d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
</encoder>
</appender>
<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<root level="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>
<root level="TRACE">
<appender-ref ref="TRACE_FILE" />
</root>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
WebSocket使用示例
集成WebSocket
pom.xml
<!-- 集成WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
新增WebSocketConfig.java
package com.li.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
新增websocket/WebSocketServer.java
package com.li.websocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 每个客户端一个token
*/
private String token = "";
private static HashMap<String, Session> map = new HashMap<>();
/**
* 连接成功
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
map.put(token, session);
this.token = token;
LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size());
}
/**
* 连接关闭
*/
@OnClose
public void onClose(Session session) {
map.remove(this.token);
LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.token, session.getId(), map.size());
}
/**
* 收到消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到消息:{},内容:{}", token, message);
}
/**
* 连接错误
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误", error);
}
/**
* 群发消息
*/
public void sendInfo(String message) {
for (String token : map.keySet()) {
Session session = map.get(token);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOG.error("推送消息失败:{},内容:{}", token, message);
}
LOG.info("推送消息:{},内容:{}", token, message);
}
}
}
修改web/.env.dev
NODE_ENV=development
VUE_APP_SERVER=http://127.0.0.1:8080
VUE_APP_WS_SERVER=ws://127.0.0.1:8080
修改the-footer.vue
<script lang="ts">
import { defineComponent, computed, onMounted } from 'vue';
import store from "@/store";
import {Tool} from "@/util/tool";
export default defineComponent({
name: 'the-footer',
setup() {
const user = computed(() => store.state.user);
let websocket: any;
let token: any;
const onOpen = () => {
console.log('WebSocket连接成功,状态码:', websocket.readyState)
};
const onMessage = (event: any) => {
console.log('WebSocket收到消息:', event.data);
};
const onError = () => {
console.log('WebSocket连接错误,状态码:', websocket.readyState)
};
const onClose = () => {
console.log('WebSocket连接关闭,状态码:', websocket.readyState)
};
const initWebSocket = () => {
// 连接成功
websocket.onopen = onOpen;
// 收到消息的回调
websocket.onmessage = onMessage;
// 连接错误
websocket.onerror = onError;
// 连接关闭的回调
websocket.onclose = onClose;
};
onMounted(() => {
// WebSocket
if ('WebSocket' in window) {
token = Tool.uuid(10);
// 连接地址:ws://127.0.0.1:8880/ws/xxx
websocket = new WebSocket(process.env.VUE_APP_WS_SERVER + '/ws/' + token);
initWebSocket()
// 关闭
// websocket.close();
} else {
alert('当前浏览器 不支持')
}
});
return {
user
}
}
});
</script>
修改tool.ts
/**
* 随机生成[len]长度的[radix]进制数
* @param len
* @param radix 默认62
* @returns {string}
*/
public static uuid (len: number, radix = 62) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
for (let i = 0; i < len; i++) {
uuid[i] = chars[0 | Math.random() * radix];
}
return uuid.join('');
}
完成点赞通知功能
修改DocService.java
@Resource
public WebSocketServer webSocketServer;
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
webSocketServer.sendInfo("【" + docDb.getName() + "】被点赞!");
修改the-footer.vue
import { notification } from 'ant-design-vue';
const onMessage = (event: any) => {
console.log('WebSocket收到消息:', event.data);
notification['info']({
message: '收到消息',
description: event.data,
});
};
使用异步化解耦点赞通知功能
- SpringBoot异步化的使用
使用异步化解耦点赞&通知功能
修改WikiApplication.java
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
修改DocService.java
@Resource
//public WebSocketServer webSocketServer;
public WsService wsService;
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
//webSocketServer.sendInfo("【" + docDb.getName() + "】被点赞!");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!");
新增WsService.java
@Service
public class WsService {
@Resource
public WebSocketServer webSocketServer;
@Async
public void sendInfo(String message) {
webSocketServer.sendInfo(message);
}
}
将业务线程日志流水号传递到异步线程,方便生产运维查问题
修改DocService.java
import org.slf4j.MDC;
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
//wsService.sendInfo("【" + docDb.getName() + "】被点赞!");
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
修改WsService.java
import org.slf4j.MDC;
@Async
//public void sendInfo(String message) {
public void sendInfo(String message, String logId) {
MDC.put("LOG_ID", logId);
webSocketServer.sendInfo(message);
}
}
加入事务注解
修改DocService.java
import org.springframework.transaction.annotation.Transactional;
@Transactional
public void save(DocSaveReq req) {
使用MQ解耦点赞通知功能
https://coding.imooc.com/lesson/474.html#mid=42509
https://git.imooc.com/coding-474/jiawawiki/commits/master?page=2&pageSize=30
使用RocketMQ解耦点赞通知功能,发送和接收调试成功
配置RocketMQ环境变量
启动mqnamesrv.cmd和mqbroker.cmd
- 路径:..rocketmqrocketmq-all-4.7.1-bin-releasebin
- 如果mqnamesrv.cmd未启动成功,请将jdk换为1.8。
- 打开两个CMD,分别进入bin目录输入
mqnamesrv
mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
pom.xml
<!-- RocketMQ-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
新增/rocketmq/VoteTopicConsumer.java
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
@RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")
public class VoteTopicConsumer implements RocketMQListener<MessageExt> {
private static final Logger LOG = LoggerFactory.getLogger(VoteTopicConsumer.class);
@Override
public void onMessage(MessageExt messageExt) {
byte[] body = messageExt.getBody();
LOG.info("ROCKETMQ收到消息:{}", new String(body));
}
}
修改DocService.java
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private DocMapperCust docMapperCust;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
@Resource
public RedisUtil redisUtil;
@Resource
public WsService wsService;
@Resource
private RocketMQTemplate rocketMQTemplate;
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
@Transactional
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
// 文档阅读数+1
docMapperCust.increaseViewCount(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 5000)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
// wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
}
public void updateEbookInfo() {
docMapperCust.updateEbookInfo();
}
}
rocketmq相关配置 - 配置application.yml
#rocketmq相关配置
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: default
使用RocketMQ解耦点赞通知功能,点赞->发MQ->收MQ->推送WS消息->弹出通知
修改VoteTopicConsumer.java
@Service
@RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")
public class VoteTopicConsumer implements RocketMQListener<MessageExt> {
private static final Logger LOG = LoggerFactory.getLogger(VoteTopicConsumer.class);
@Resource
public WebSocketServer webSocketServer;
@Override
public void onMessage(MessageExt messageExt) {
byte[] body = messageExt.getBody();
LOG.info("ROCKETMQ收到消息:{}", new String(body));
webSocketServer.sendInfo(new String(body));
}
}
针对点赞通知功能,使用异步化够了,没必要引入MQ
注释RocketMQ依赖
<!-- RocketMQ-->
<!--<dependency>-->
<!-- <groupId>org.apache.rocketmq</groupId>-->
<!-- <artifactId>rocketmq-spring-boot-starter</artifactId>-->
<!-- <version>2.0.2</version>-->
<!--</dependency>-->
注释VoteTopicConsumer.java
修改DocService.java
@Service
public class DocService {
private static final Logger LOG = LoggerFactory.getLogger(DocService.class);
@Resource
private DocMapper docMapper;
@Resource
private DocMapperCust docMapperCust;
@Resource
private ContentMapper contentMapper;
@Resource
private SnowFlake snowFlake;
@Resource
public RedisUtil redisUtil;
@Resource
public WsService wsService;
// @Resource
// private RocketMQTemplate rocketMQTemplate;
public List<DocQueryResp> all(Long ebookId) {
DocExample docExample = new DocExample();
docExample.createCriteria().andEbookIdEqualTo(ebookId);
docExample.setOrderByClause("sort asc");
List<Doc> docList = docMapper.selectByExample(docExample);
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
return list;
}
public PageResp<DocQueryResp> list(DocQueryReq req) {
DocExample docExample = new DocExample();
docExample.setOrderByClause("sort asc");
DocExample.Criteria criteria = docExample.createCriteria();
PageHelper.startPage(req.getPage(), req.getSize());
List<Doc> docList = docMapper.selectByExample(docExample);
PageInfo<Doc> pageInfo = new PageInfo<>(docList);
LOG.info("总行数:{}", pageInfo.getTotal());
LOG.info("总页数:{}", pageInfo.getPages());
// 列表复制
List<DocQueryResp> list = CopyUtil.copyList(docList, DocQueryResp.class);
PageResp<DocQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(list);
return pageResp;
}
/**
* 保存
*/
@Transactional
public void save(DocSaveReq req) {
Doc doc = CopyUtil.copy(req, Doc.class);
Content content = CopyUtil.copy(req, Content.class);
if (ObjectUtils.isEmpty(req.getId())) {
// 新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insert(doc);
content.setId(doc.getId());
contentMapper.insert(content);
} else {
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
}
}
public void delete(Long id) {
docMapper.deleteByPrimaryKey(id);
}
public void delete(List<String> ids) {
DocExample docExample = new DocExample();
DocExample.Criteria criteria = docExample.createCriteria();
criteria.andIdIn(ids);
docMapper.deleteByExample(docExample);
}
public String findContent(Long id) {
Content content = contentMapper.selectByPrimaryKey(id);
// 文档阅读数+1
docMapperCust.increaseViewCount(id);
if (ObjectUtils.isEmpty(content)) {
return "";
} else {
return content.getContent();
}
}
/**
* 点赞
*/
public void vote(Long id) {
// docMapperCust.increaseVoteCount(id);
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
if (redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 5000)) {
docMapperCust.increaseVoteCount(id);
} else {
throw new BusinessException(BusinessExceptionCode.VOTE_REPEAT);
}
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
// rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
}
public void updateEbookInfo() {
docMapperCust.updateEbookInfo();
}
}
注释rocketmq相关配置 - application.yml
#rocketmq相关配置
#rocketmq:
#name-server: 127.0.0.1:9876
#producer:
#group: default
十六、知识库功能开发 - 统计数据收集与Echarts报表(82分钟)
报表统计方案的探讨
统计维度
统计数值
- 总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长
统计报表
- 30天阅读/点赞趋势图
- 文档阅读量排名(热门文章)、文档点赞量排名(优质文章)
如何统计
业务表统计:所有报表数据都是从业务表直接获取的
- 优点::实时性好(数据准确),工作量?
- 缺点:对业务表性能有影响、有些统计无法实现
中间表统计:定时将业务表数据汇总到中间表,报表数据从中间表获取
- 优点:性能好、可实现多功能统计
- 缺点:工作量大,步骤多容易出错
电子书快照表设计
增加电子书快照表,生成持久层代码
SQL
-- 电子书快照表
drop table if exists `ebook_snapshot`;
create table `ebook_snapshot` (
`id` bigint auto_increment not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`date` date not null comment '快照日期',
`view_count` int not null default 0 comment '阅读数',
`vote_count` int not null default 0 comment '点赞数',
`view_increase` int not null default 0 comment '阅读增长',
`vote_increase` int not null default 0 comment '点赞增长',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='电子书快照表';
/entity/EbookSnapshot.java
package com.li.entity;
import java.util.Date;
public class EbookSnapshot {
private Long id;
private Long ebookId;
private Date date;
private Integer viewCount;
private Integer voteCount;
private Integer viewIncrease;
private Integer voteIncrease;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getEbookId() {
return ebookId;
}
public void setEbookId(Long ebookId) {
this.ebookId = ebookId;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
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;
}
public Integer getViewIncrease() {
return viewIncrease;
}
public void setViewIncrease(Integer viewIncrease) {
this.viewIncrease = viewIncrease;
}
public Integer getVoteIncrease() {
return voteIncrease;
}
public void setVoteIncrease(Integer voteIncrease) {
this.voteIncrease = voteIncrease;
}
@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(", ebookId=").append(ebookId);
sb.append(", date=").append(date);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append(", viewIncrease=").append(viewIncrease);
sb.append(", voteIncrease=").append(voteIncrease);
sb.append("]");
return sb.toString();
}
}
EbookSnapshotExample.java
package com.li.entity;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
public class EbookSnapshotExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public EbookSnapshotExample() {
oredCriteria = new ArrayList<>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
protected void addCriterionForJDBCDate(String condition, Date value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
addCriterion(condition, new java.sql.Date(value.getTime()), property);
}
protected void addCriterionForJDBCDate(String condition, List<Date> values, String property) {
if (values == null || values.size() == 0) {
throw new RuntimeException("Value list for " + property + " cannot be null or empty");
}
List<java.sql.Date> dateList = new ArrayList<>();
Iterator<Date> iter = values.iterator();
while (iter.hasNext()) {
dateList.add(new java.sql.Date(iter.next().getTime()));
}
addCriterion(condition, dateList, property);
}
protected void addCriterionForJDBCDate(String condition, Date value1, Date value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
addCriterion(condition, new java.sql.Date(value1.getTime()), new java.sql.Date(value2.getTime()), property);
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(Long value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(Long value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(Long value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(Long value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(Long value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(Long value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<Long> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<Long> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(Long value1, Long value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(Long value1, Long value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andEbookIdIsNull() {
addCriterion("ebook_id is null");
return (Criteria) this;
}
public Criteria andEbookIdIsNotNull() {
addCriterion("ebook_id is not null");
return (Criteria) this;
}
public Criteria andEbookIdEqualTo(Long value) {
addCriterion("ebook_id =", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotEqualTo(Long value) {
addCriterion("ebook_id <>", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdGreaterThan(Long value) {
addCriterion("ebook_id >", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdGreaterThanOrEqualTo(Long value) {
addCriterion("ebook_id >=", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdLessThan(Long value) {
addCriterion("ebook_id <", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdLessThanOrEqualTo(Long value) {
addCriterion("ebook_id <=", value, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdIn(List<Long> values) {
addCriterion("ebook_id in", values, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotIn(List<Long> values) {
addCriterion("ebook_id not in", values, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdBetween(Long value1, Long value2) {
addCriterion("ebook_id between", value1, value2, "ebookId");
return (Criteria) this;
}
public Criteria andEbookIdNotBetween(Long value1, Long value2) {
addCriterion("ebook_id not between", value1, value2, "ebookId");
return (Criteria) this;
}
public Criteria andDateIsNull() {
addCriterion("`date` is null");
return (Criteria) this;
}
public Criteria andDateIsNotNull() {
addCriterion("`date` is not null");
return (Criteria) this;
}
public Criteria andDateEqualTo(Date value) {
addCriterionForJDBCDate("`date` =", value, "date");
return (Criteria) this;
}
public Criteria andDateNotEqualTo(Date value) {
addCriterionForJDBCDate("`date` <>", value, "date");
return (Criteria) this;
}
public Criteria andDateGreaterThan(Date value) {
addCriterionForJDBCDate("`date` >", value, "date");
return (Criteria) this;
}
public Criteria andDateGreaterThanOrEqualTo(Date value) {
addCriterionForJDBCDate("`date` >=", value, "date");
return (Criteria) this;
}
public Criteria andDateLessThan(Date value) {
addCriterionForJDBCDate("`date` <", value, "date");
return (Criteria) this;
}
public Criteria andDateLessThanOrEqualTo(Date value) {
addCriterionForJDBCDate("`date` <=", value, "date");
return (Criteria) this;
}
public Criteria andDateIn(List<Date> values) {
addCriterionForJDBCDate("`date` in", values, "date");
return (Criteria) this;
}
public Criteria andDateNotIn(List<Date> values) {
addCriterionForJDBCDate("`date` not in", values, "date");
return (Criteria) this;
}
public Criteria andDateBetween(Date value1, Date value2) {
addCriterionForJDBCDate("`date` between", value1, value2, "date");
return (Criteria) this;
}
public Criteria andDateNotBetween(Date value1, Date value2) {
addCriterionForJDBCDate("`date` not between", value1, value2, "date");
return (Criteria) this;
}
public Criteria andViewCountIsNull() {
addCriterion("view_count is null");
return (Criteria) this;
}
public Criteria andViewCountIsNotNull() {
addCriterion("view_count is not null");
return (Criteria) this;
}
public Criteria andViewCountEqualTo(Integer value) {
addCriterion("view_count =", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotEqualTo(Integer value) {
addCriterion("view_count <>", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountGreaterThan(Integer value) {
addCriterion("view_count >", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountGreaterThanOrEqualTo(Integer value) {
addCriterion("view_count >=", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountLessThan(Integer value) {
addCriterion("view_count <", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountLessThanOrEqualTo(Integer value) {
addCriterion("view_count <=", value, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountIn(List<Integer> values) {
addCriterion("view_count in", values, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotIn(List<Integer> values) {
addCriterion("view_count not in", values, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountBetween(Integer value1, Integer value2) {
addCriterion("view_count between", value1, value2, "viewCount");
return (Criteria) this;
}
public Criteria andViewCountNotBetween(Integer value1, Integer value2) {
addCriterion("view_count not between", value1, value2, "viewCount");
return (Criteria) this;
}
public Criteria andVoteCountIsNull() {
addCriterion("vote_count is null");
return (Criteria) this;
}
public Criteria andVoteCountIsNotNull() {
addCriterion("vote_count is not null");
return (Criteria) this;
}
public Criteria andVoteCountEqualTo(Integer value) {
addCriterion("vote_count =", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotEqualTo(Integer value) {
addCriterion("vote_count <>", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountGreaterThan(Integer value) {
addCriterion("vote_count >", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountGreaterThanOrEqualTo(Integer value) {
addCriterion("vote_count >=", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountLessThan(Integer value) {
addCriterion("vote_count <", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountLessThanOrEqualTo(Integer value) {
addCriterion("vote_count <=", value, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountIn(List<Integer> values) {
addCriterion("vote_count in", values, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotIn(List<Integer> values) {
addCriterion("vote_count not in", values, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountBetween(Integer value1, Integer value2) {
addCriterion("vote_count between", value1, value2, "voteCount");
return (Criteria) this;
}
public Criteria andVoteCountNotBetween(Integer value1, Integer value2) {
addCriterion("vote_count not between", value1, value2, "voteCount");
return (Criteria) this;
}
public Criteria andViewIncreaseIsNull() {
addCriterion("view_increase is null");
return (Criteria) this;
}
public Criteria andViewIncreaseIsNotNull() {
addCriterion("view_increase is not null");
return (Criteria) this;
}
public Criteria andViewIncreaseEqualTo(Integer value) {
addCriterion("view_increase =", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseNotEqualTo(Integer value) {
addCriterion("view_increase <>", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseGreaterThan(Integer value) {
addCriterion("view_increase >", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseGreaterThanOrEqualTo(Integer value) {
addCriterion("view_increase >=", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseLessThan(Integer value) {
addCriterion("view_increase <", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseLessThanOrEqualTo(Integer value) {
addCriterion("view_increase <=", value, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseIn(List<Integer> values) {
addCriterion("view_increase in", values, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseNotIn(List<Integer> values) {
addCriterion("view_increase not in", values, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseBetween(Integer value1, Integer value2) {
addCriterion("view_increase between", value1, value2, "viewIncrease");
return (Criteria) this;
}
public Criteria andViewIncreaseNotBetween(Integer value1, Integer value2) {
addCriterion("view_increase not between", value1, value2, "viewIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseIsNull() {
addCriterion("vote_increase is null");
return (Criteria) this;
}
public Criteria andVoteIncreaseIsNotNull() {
addCriterion("vote_increase is not null");
return (Criteria) this;
}
public Criteria andVoteIncreaseEqualTo(Integer value) {
addCriterion("vote_increase =", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseNotEqualTo(Integer value) {
addCriterion("vote_increase <>", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseGreaterThan(Integer value) {
addCriterion("vote_increase >", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseGreaterThanOrEqualTo(Integer value) {
addCriterion("vote_increase >=", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseLessThan(Integer value) {
addCriterion("vote_increase <", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseLessThanOrEqualTo(Integer value) {
addCriterion("vote_increase <=", value, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseIn(List<Integer> values) {
addCriterion("vote_increase in", values, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseNotIn(List<Integer> values) {
addCriterion("vote_increase not in", values, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseBetween(Integer value1, Integer value2) {
addCriterion("vote_increase between", value1, value2, "voteIncrease");
return (Criteria) this;
}
public Criteria andVoteIncreaseNotBetween(Integer value1, Integer value2) {
addCriterion("vote_increase not between", value1, value2, "voteIncrease");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}
EbookSnapshotMapper.java
import org.apache.ibatis.annotations.Param;
public interface EbookSnapshotMapper {
long countByExample(EbookSnapshotExample example);
int deleteByExample(EbookSnapshotExample example);
int deleteByPrimaryKey(Long id);
int insert(EbookSnapshot record);
int insertSelective(EbookSnapshot record);
List<EbookSnapshot> selectByExample(EbookSnapshotExample example);
EbookSnapshot selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param("record") EbookSnapshot record, @Param("example") EbookSnapshotExample example);
int updateByExample(@Param("record") EbookSnapshot record, @Param("example") EbookSnapshotExample example);
int updateByPrimaryKeySelective(EbookSnapshot record);
int updateByPrimaryKey(EbookSnapshot record);
}
/resources/mapper/EbookSnapshotMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.li.mapper.EbookSnapshotMapper">
<resultMap id="BaseResultMap" type="com.li.entity.EbookSnapshot">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="ebook_id" jdbcType="BIGINT" property="ebookId" />
<result column="date" jdbcType="DATE" property="date" />
<result column="view_count" jdbcType="INTEGER" property="viewCount" />
<result column="vote_count" jdbcType="INTEGER" property="voteCount" />
<result column="view_increase" jdbcType="INTEGER" property="viewIncrease" />
<result column="vote_increase" jdbcType="INTEGER" property="voteIncrease" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Update_By_Example_Where_Clause">
<where>
<foreach collection="example.oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Base_Column_List">
id, ebook_id, `date`, view_count, vote_count, view_increase, vote_increase
</sql>
<select id="selectByExample" parameterType="com.li.entity.EbookSnapshotExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from ebook_snapshot
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from ebook_snapshot
where id = #{id,jdbcType=BIGINT}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
delete from ebook_snapshot
where id = #{id,jdbcType=BIGINT}
</delete>
<delete id="deleteByExample" parameterType="com.li.entity.EbookSnapshotExample">
delete from ebook_snapshot
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</delete>
<insert id="insert" parameterType="com.li.entity.EbookSnapshot">
insert into ebook_snapshot (id, ebook_id, `date`,
view_count, vote_count, view_increase,
vote_increase)
values (#{id,jdbcType=BIGINT}, #{ebookId,jdbcType=BIGINT}, #{date,jdbcType=DATE},
#{viewCount,jdbcType=INTEGER}, #{voteCount,jdbcType=INTEGER}, #{viewIncrease,jdbcType=INTEGER},
#{voteIncrease,jdbcType=INTEGER})
</insert>
<insert id="insertSelective" parameterType="com.li.entity.EbookSnapshot">
insert into ebook_snapshot
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="ebookId != null">
ebook_id,
</if>
<if test="date != null">
`date`,
</if>
<if test="viewCount != null">
view_count,
</if>
<if test="voteCount != null">
vote_count,
</if>
<if test="viewIncrease != null">
view_increase,
</if>
<if test="voteIncrease != null">
vote_increase,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
</if>
<if test="ebookId != null">
#{ebookId,jdbcType=BIGINT},
</if>
<if test="date != null">
#{date,jdbcType=DATE},
</if>
<if test="viewCount != null">
#{viewCount,jdbcType=INTEGER},
</if>
<if test="voteCount != null">
#{voteCount,jdbcType=INTEGER},
</if>
<if test="viewIncrease != null">
#{viewIncrease,jdbcType=INTEGER},
</if>
<if test="voteIncrease != null">
#{voteIncrease,jdbcType=INTEGER},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="com.li.entity.EbookSnapshotExample" resultType="java.lang.Long">
select count(*) from ebook_snapshot
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</select>
<update id="updateByExampleSelective" parameterType="map">
update ebook_snapshot
<set>
<if test="record.id != null">
id = #{record.id,jdbcType=BIGINT},
</if>
<if test="record.ebookId != null">
ebook_id = #{record.ebookId,jdbcType=BIGINT},
</if>
<if test="record.date != null">
`date` = #{record.date,jdbcType=DATE},
</if>
<if test="record.viewCount != null">
view_count = #{record.viewCount,jdbcType=INTEGER},
</if>
<if test="record.voteCount != null">
vote_count = #{record.voteCount,jdbcType=INTEGER},
</if>
<if test="record.viewIncrease != null">
view_increase = #{record.viewIncrease,jdbcType=INTEGER},
</if>
<if test="record.voteIncrease != null">
vote_increase = #{record.voteIncrease,jdbcType=INTEGER},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExample" parameterType="map">
update ebook_snapshot
set id = #{record.id,jdbcType=BIGINT},
ebook_id = #{record.ebookId,jdbcType=BIGINT},
`date` = #{record.date,jdbcType=DATE},
view_count = #{record.viewCount,jdbcType=INTEGER},
vote_count = #{record.voteCount,jdbcType=INTEGER},
view_increase = #{record.viewIncrease,jdbcType=INTEGER},
vote_increase = #{record.voteIncrease,jdbcType=INTEGER}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="com.li.entity.EbookSnapshot">
update ebook_snapshot
<set>
<if test="ebookId != null">
ebook_id = #{ebookId,jdbcType=BIGINT},
</if>
<if test="date != null">
`date` = #{date,jdbcType=DATE},
</if>
<if test="viewCount != null">
view_count = #{viewCount,jdbcType=INTEGER},
</if>
<if test="voteCount != null">
vote_count = #{voteCount,jdbcType=INTEGER},
</if>
<if test="viewIncrease != null">
view_increase = #{viewIncrease,jdbcType=INTEGER},
</if>
<if test="voteIncrease != null">
vote_increase = #{voteIncrease,jdbcType=INTEGER},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
<update id="updateByPrimaryKey" parameterType="com.li.entity.EbookSnapshot">
update ebook_snapshot
set ebook_id = #{ebookId,jdbcType=BIGINT},
`date` = #{date,jdbcType=DATE},
view_count = #{viewCount,jdbcType=INTEGER},
vote_count = #{voteCount,jdbcType=INTEGER},
view_increase = #{viewIncrease,jdbcType=INTEGER},
vote_increase = #{voteIncrease,jdbcType=INTEGER}
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>
generator-config.xml
<!--<table tableName="user"/>-->
<table tableName="ebook_snapshot"/>
电子书快照收集脚本编写
电子书快照表增加唯一键,一个电子书一天只能有一条记录
sql
-- 电子书快照表
drop table if exists `ebook_snapshot`;
create table `ebook_snapshot` (
`id` bigint auto_increment not null comment 'id',
`ebook_id` bigint not null default 0 comment '电子书id',
`date` date not null comment '快照日期',
`view_count` int not null default 0 comment '阅读数',
`vote_count` int not null default 0 comment '点赞数',
`view_increase` int not null default 0 comment '阅读增长',
`vote_increase` int not null default 0 comment '点赞增长',
primary key (`id`),
unique key `ebook_id_date_unique` (`ebook_id`, `date`)
) engine=innodb default charset=utf8mb4 comment='电子书快照表';
完成电子书快照功能
增加定时任务,定时收集数据
新增job/EbookSnapshotJob.java
@Component
public class EbookSnapshotJob {
private static final Logger LOG = LoggerFactory.getLogger(EbookSnapshotJob.class);
@Resource
private EbookSnapshotService ebookSnapshotService;
@Resource
private SnowFlake snowFlake;
/**
* 自定义cron表达式跑批
* 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
*/
@Scheduled(cron = "0/5 * * * * ?")
public void doSnapshot() {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
LOG.info("生成今日电子书快照开始");
Long start = System.currentTimeMillis();
ebookSnapshotService.genSnapshot();
LOG.info("生成今日电子书快照结束,耗时:{}毫秒", System.currentTimeMillis() - start);
}
}
新增/mapper/EbookSnapshotMapperCust.java
public interface EbookSnapshotMapperCust {
public void genSnapshot();
}
新增EbookSnapshotService.java
@Service
public class EbookSnapshotService {
@Resource
private EbookSnapshotMapperCust ebookSnapshotMapperCust;
public void genSnapshot() {
ebookSnapshotMapperCust.genSnapshot();
}
}
修改数据库连接配置 - application.properties
- &allowMultiQueries=true
# 数据库连接配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3307/wiki?serverTimezone=UTC&allowMultiQueries=true
username: root
password: 1234
新增/resources/mapper/EbookSnapshotMapperCust.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.li.mapper.EbookSnapshotMapperCust" >
<!--
# 方案一(ID不连续):
# 删除今天的数据
# 为所有的电子书生成一条今天的记录
# 更新总阅读数、总点赞数
# 更新今日阅读数、今日点赞数
# 方案二(ID连续):
# 为所有的电子书生成一条今天的记录,如果还没有
# 更新总阅读数、总点赞数
# 更新今日阅读数、今日点赞数
-->
<update id="genSnapshot">
insert into ebook_snapshot(ebook_id, `date`, view_count, vote_count, view_increase, vote_increase)
select t1.id, curdate(), 0, 0, 0, 0
from ebook t1
where not exists(select 1
from ebook_snapshot t2
where t1.id = t2.ebook_id
and t2.`date` = curdate());
update ebook_snapshot t1, ebook t2
set t1.view_count = t2.view_count,
t1.vote_count = t2.vote_count
where t1.`date` = curdate()
and t1.ebook_id = t2.id;
update ebook_snapshot t1 left join (select ebook_id, view_count, vote_count
from ebook_snapshot
where `date` = date_sub(curdate(), interval 1 day)) t2
on t1.ebook_id = t2.ebook_id
set t1.view_increase = (t1.view_count - ifnull(t2.view_count, 0)),
t1.vote_increase = (t1.vote_count - ifnull(t2.vote_count, 0))
where t1.`date` = curdate();
</update>
</mapper>
首页统计数值功能开发
后端增加获取统计数值接口
新增EbookSnapshotController.java
@RestController
@RequestMapping("/ebook-snapshot")
public class EbookSnapshotController {
@Resource
private EbookSnapshotService ebookSnapshotService;
@GetMapping("/get-statistic")
public CommonResp getStatistic() {
List<StatisticResp> statisticResp = ebookSnapshotService.getStatistic();
CommonResp<List<StatisticResp>> commonResp = new CommonResp<>();
commonResp.setContent(statisticResp);
return commonResp;
}
修改EbookSnapshotMapperCust.java
public interface EbookSnapshotMapperCust {
public void genSnapshot();
List<StatisticResp> getStatistic();
}
新增StatisticResp.java
package com.li.response;
import java.util.Date;
public class StatisticResp {
private Date date;
private int viewCount;
private int voteCount;
private int viewIncrease;
private int voteIncrease;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public int getViewCount() {
return viewCount;
}
public void setViewCount(int viewCount) {
this.viewCount = viewCount;
}
public int getVoteCount() {
return voteCount;
}
public void setVoteCount(int voteCount) {
this.voteCount = voteCount;
}
public int getViewIncrease() {
return viewIncrease;
}
public void setViewIncrease(int viewIncrease) {
this.viewIncrease = viewIncrease;
}
public int getVoteIncrease() {
return voteIncrease;
}
public void setVoteIncrease(int voteIncrease) {
this.voteIncrease = voteIncrease;
}
@Override
public String toString() {
return "StatisticResp{" +
"date=" + date +
", viewCount=" + viewCount +
", voteCount=" + voteCount +
", viewIncrease=" + viewIncrease +
", voteIncrease=" + voteIncrease +
'}';
}
}
修改EbookSnapshotService.java
@Service
public class EbookSnapshotService {
@Resource
private EbookSnapshotMapperCust ebookSnapshotMapperCust;
public void genSnapshot() {
ebookSnapshotMapperCust.genSnapshot();
}
/**
* 获取首页数值数据:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长
*/
public List<StatisticResp> getStatistic() {
return ebookSnapshotMapperCust.getStatistic();
}
}
修改resources/mapper/EbookSnapshotMapperCust.xml
<!-- 获取首页数值数据:总阅读数、总点赞数、今日阅读数、今日点赞数、今日预计阅读数、今日预计阅读增长 -->
<select id="getStatistic" resultType="com.li.response.StatisticResp">
select
t1.`date` as `date`,
sum(t1.view_count) as viewCount,
sum(t1.vote_count) as voteCount,
sum(t1.view_increase) as viewIncrease,
sum(t1.vote_increase) as voteIncrease
from
ebook_snapshot t1
where
t1.`date` >= date_sub(curdate(), interval 1 day)
group by
t1.`date`
order by
t1.`date` asc;
</select>
将欢迎文字提取成组件
新建components/the-welcome.vue
<template>
<h1>欢迎使用知识库1</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'the-welcome',
setup () {
return {
}
}
});
</script>
修改home.vue
<div class="welcome" v-show="isShowWelcome">
<!--
<h1>欢迎使用知识库</h1>-->
<the-welcome></the-welcome>
</div>
import TheWelcome from '@/components/the-welcome.vue';
export default defineComponent({
name: 'Home',
components: {
TheWelcome
},
setup() {
const ebooks = ref();
前端增加数值统计组件,显示统计数值
修改SpringMvcConfig.java
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/vote/**",
"/doc/find-content/**",
"/ebook-snapshot/**"
);
}
}
修改the-welcome.vue
<template>
<div>
<a-row>
<a-col :span="24">
<a-card>
<a-row>
<a-col :span="8">
<a-statistic title="总阅读量" :value="statistic.viewCount">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="总点赞量" :value="statistic.voteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="点赞率" :value="statistic.voteCount / statistic.viewCount * 100"
:precision="2"
suffix="%"
:value-style="{ color: '#cf1322' }">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row :gutter="16">
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic title="今日阅读" :value="statistic.todayViewCount" style="margin-right: 50px">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日点赞" :value="statistic.todayVoteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic
title="预计今日阅读"
:value="statistic.todayViewIncrease"
:value-style="{ color: '#0000ff' }"
>
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic
title="预计今日阅读增长"
:value="statistic.todayViewIncreaseRateAbs"
:precision="2"
suffix="%"
class="demo-class"
:value-style="statistic.todayViewIncreaseRate < 0 ? { color: '#3f8600' } : { color: '#cf1322' }"
>
<template #prefix>
<arrow-down-outlined v-if="statistic.todayViewIncreaseRate < 0"/>
<arrow-up-outlined v-if="statistic.todayViewIncreaseRate >= 0"/>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import axios from 'axios';
export default defineComponent({
name: 'the-welcome',
setup () {
const statistic = ref();
statistic.value = {};
const getStatistic = () => {
axios.get('/ebook-snapshot/get-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticResp = data.content;
statistic.value.viewCount = statisticResp[1].viewCount;
statistic.value.voteCount = statisticResp[1].voteCount;
statistic.value.todayViewCount = statisticResp[1].viewIncrease;
statistic.value.todayVoteCount = statisticResp[1].voteIncrease;
// 按分钟计算当前时间点,占一天的百分比
const now = new Date();
const nowRate = (now.getHours() * 60 + now.getMinutes()) / (60 * 24);
// console.log(nowRate)
statistic.value.todayViewIncrease = parseInt(String(statisticResp[1].viewIncrease / nowRate));
// todayViewIncreaseRate:今日预计增长率
statistic.value.todayViewIncreaseRate = (statistic.value.todayViewIncrease - statisticResp[0].viewIncrease) / statisticResp[0].viewIncrease * 100;
statistic.value.todayViewIncreaseRateAbs = Math.abs(statistic.value.todayViewIncreaseRate);
}
});
};
onMounted(() => {
getStatistic();
});
return {
statistic
}
}
});
</script>
Echarts的集成与使用示例
集成echarts5.0.2,增加示例
修改index.html
<script src="<%= BASE_URL %>js/echarts_5.0.2.min.js"></script>
<title><%= htmlWebpackPlugin.options.title %></title>
引入echarts_5.0.2.min.js
修改the-welcome.vue
<template>
<div>
<a-row>
<a-col :span="24">
<a-card>
<a-row>
<a-col :span="8">
<a-statistic title="总阅读量" :value="statistic.viewCount">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="总点赞量" :value="statistic.voteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="点赞率" :value="statistic.voteCount / statistic.viewCount * 100"
:precision="2"
suffix="%"
:value-style="{ color: '#cf1322' }">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row :gutter="16">
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic title="今日阅读" :value="statistic.todayViewCount" style="margin-right: 50px">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日点赞" :value="statistic.todayVoteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic
title="预计今日阅读"
:value="statistic.todayViewIncrease"
:value-style="{ color: '#0000ff' }"
>
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic
title="预计今日阅读增长"
:value="statistic.todayViewIncreaseRateAbs"
:precision="2"
suffix="%"
class="demo-class"
:value-style="statistic.todayViewIncreaseRate < 0 ? { color: '#3f8600' } : { color: '#cf1322' }"
>
<template #prefix>
<arrow-down-outlined v-if="statistic.todayViewIncreaseRate < 0"/>
<arrow-up-outlined v-if="statistic.todayViewIncreaseRate >= 0"/>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row>
<a-col :span="24">
<div id="main" style="width: 100%;height:300px;"></div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import axios from 'axios';
declare let echarts: any;
export default defineComponent({
name: 'the-welcome',
setup () {
const statistic = ref();
statistic.value = {};
const getStatistic = () => {
axios.get('/ebook-snapshot/get-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticResp = data.content;
statistic.value.viewCount = statisticResp[1].viewCount;
statistic.value.voteCount = statisticResp[1].voteCount;
statistic.value.todayViewCount = statisticResp[1].viewIncrease;
statistic.value.todayVoteCount = statisticResp[1].voteIncrease;
// 按分钟计算当前时间点,占一天的百分比
const now = new Date();
const nowRate = (now.getHours() * 60 + now.getMinutes()) / (60 * 24);
// console.log(nowRate)
statistic.value.todayViewIncrease = parseInt(String(statisticResp[1].viewIncrease / nowRate));
// todayViewIncreaseRate:今日预计增长率
statistic.value.todayViewIncreaseRate = (statistic.value.todayViewIncrease - statisticResp[0].viewIncrease) / statisticResp[0].viewIncrease * 100;
statistic.value.todayViewIncreaseRateAbs = Math.abs(statistic.value.todayViewIncreaseRate);
}
});
};
const testEcharts = () => {
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
};
onMounted(() => {
getStatistic();
testEcharts();
});
return {
statistic
}
}
});
</script>
30天趋势图功能开发
增加获取30天阅读数点赞数接口
修改EbookSnapshotController.java
@GetMapping("/get-30-statistic")
public CommonResp get30Statistic() {
List<StatisticResp> statisticResp = ebookSnapshotService.get30Statistic();
CommonResp<List<StatisticResp>> commonResp = new CommonResp<>();
commonResp.setContent(statisticResp);
return commonResp;
}
修改EbookSnapshotMapperCust.java
List<StatisticResp> get30Statistic();
修改EbookSnapshotService.java
/**
* 30天数值统计
*/
public List<StatisticResp> get30Statistic() {
return ebookSnapshotMapperCust.get30Statistic();
}
修改/resources/mapper/EbookSnapshotMapperCust.xml
<select id="get30Statistic" resultType="com.li.response.StatisticResp">
select
t1.`date` as `date`,
sum(t1.view_increase) as viewIncrease,
sum(t1.vote_increase) as voteIncrease
from
ebook_snapshot t1
where
t1.`date` between date_sub(curdate(), interval 30 day) and date_sub(curdate(), interval 1 day)
group by
t1.`date`
order by
t1.`date` asc;
</select>
展示30天趋势图
修改StatisticResp.java
import com.fasterxml.jackson.annotation.JsonFormat;
public class StatisticResp {
@JsonFormat(pattern="MM-dd", timezone = "GMT+8")
private Date date;
修改the-welcome.vue
<template>
<div>
<a-row>
<a-col :span="24">
<a-card>
<a-row>
<a-col :span="8">
<a-statistic title="总阅读量" :value="statistic.viewCount">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="总点赞量" :value="statistic.voteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
<a-col :span="8">
<a-statistic title="点赞率" :value="statistic.voteCount / statistic.viewCount * 100"
:precision="2"
suffix="%"
:value-style="{ color: '#cf1322' }">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row :gutter="16">
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic title="今日阅读" :value="statistic.todayViewCount" style="margin-right: 50px">
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic title="今日点赞" :value="statistic.todayVoteCount">
<template #suffix>
<like-outlined />
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :span="12">
<a-card>
<a-row>
<a-col :span="12">
<a-statistic
title="预计今日阅读"
:value="statistic.todayViewIncrease"
:value-style="{ color: '#0000ff' }"
>
<template #suffix>
<UserOutlined />
</template>
</a-statistic>
</a-col>
<a-col :span="12">
<a-statistic
title="预计今日阅读增长"
:value="statistic.todayViewIncreaseRateAbs"
:precision="2"
suffix="%"
class="demo-class"
:value-style="statistic.todayViewIncreaseRate < 0 ? { color: '#3f8600' } : { color: '#cf1322' }"
>
<template #prefix>
<arrow-down-outlined v-if="statistic.todayViewIncreaseRate < 0"/>
<arrow-up-outlined v-if="statistic.todayViewIncreaseRate >= 0"/>
</template>
</a-statistic>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<br>
<a-row>
<a-col :span="24">
<div id="main" style="width: 100%;height:300px;"></div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import axios from 'axios';
declare let echarts: any;
export default defineComponent({
name: 'the-welcome',
setup () {
const statistic = ref();
statistic.value = {};
const getStatistic = () => {
axios.get('/ebook-snapshot/get-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticResp = data.content;
statistic.value.viewCount = statisticResp[1].viewCount;
statistic.value.voteCount = statisticResp[1].voteCount;
statistic.value.todayViewCount = statisticResp[1].viewIncrease;
statistic.value.todayVoteCount = statisticResp[1].voteIncrease;
// 按分钟计算当前时间点,占一天的百分比
const now = new Date();
const nowRate = (now.getHours() * 60 + now.getMinutes()) / (60 * 24);
// console.log(nowRate)
statistic.value.todayViewIncrease = parseInt(String(statisticResp[1].viewIncrease / nowRate));
// todayViewIncreaseRate:今日预计增长率
statistic.value.todayViewIncreaseRate = (statistic.value.todayViewIncrease - statisticResp[0].viewIncrease) / statisticResp[0].viewIncrease * 100;
statistic.value.todayViewIncreaseRateAbs = Math.abs(statistic.value.todayViewIncreaseRate);
}
});
};
const init30DayEcharts = (list: any) => {
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById('main'));
const xAxis = [];
const seriesView = [];
const seriesVote = [];
for (let i = 0; i < list.length; i++) {
const record = list[i];
xAxis.push(record.date);
seriesView.push(record.viewIncrease);
seriesVote.push(record.voteIncrease);
}
// 指定图表的配置项和数据
const option = {
title: {
text: '30天趋势图'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['总阅读量', '总点赞量']
},
grid: {
left: '1%',
right: '3%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxis
},
yAxis: {
type: 'value'
},
series: [
{
name: '总阅读量',
type: 'line',
// stack: '总量', 不堆叠
data: seriesView,
smooth: true
},
{
name: '总点赞量',
type: 'line',
// stack: '总量', 不堆叠
data: seriesVote,
smooth: true
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
};
const get30DayStatistic = () => {
axios.get('/ebook-snapshot/get-30-statistic').then((response) => {
const data = response.data;
if (data.success) {
const statisticList = data.content;
init30DayEcharts(statisticList)
}
});
};
const testEcharts = () => {
// 基于准备好的dom,初始化echarts实例
const myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
const option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
};
onMounted(() => {
getStatistic();
// testEcharts();
get30DayStatistic();
});
return {
statistic
}
}
});
</script>
网站优化
修改LOGO展示
注释/删除样式 - App.vue
<!--<style>
#components-layout-demo-top-side-2 .logo {
width: 120px;
height: 31px;
background: rgba(255, 255, 255, 0.2);
margin: 16px 28px 16px 0;
float: left;
}
</style>-->
修改the-header.vue
<template>
<a-layout-header class="header">
<!-- <div class="logo" />-->
<div class="logo">知识库</div>
<!------------------->
<style>
.logo {
width: 120px;
height: 31px;
/*background: rgba(255, 255, 255, 0.2);*/
/*margin: 16px 28px 16px 0;*/
float: left;
color: white;
font-size: 18px;
}
.login-menu {
float: right;
color: white;
padding-left: 10px;
}
</style>
首次加载,给出等待提示
修改index.html
<div id="app">
<div style="width: 400px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin: -50px 0 0 -200px;font-family: YouYuan;
color: rgba(0, 0, 0, 1) !important;
font-size: 20px !important;
font-weight: 400;">
首次加载会较慢,精彩内容马上呈现...
</div>
</div>
富文本框图片自适应
修改doc.vue
/* 图片自适应 */
.wangeditor img {
max-width: 100%;
height: auto;
}
关于我们文案修改
修改about.vue
<template>
<a-layout>
<a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }">
<h1>毕设项目</h1>
</a-layout-content>
</a-layout>
</template>
BUG修复,如果一个电子书初始没有文档,则父文档选择框无法选中“无”
修改admin-doc.vue
// 父文档下拉框初始化,相当于点击新增
<!--treeSelectData.value = Tool.copy(level1.value);-->
treeSelectData.value = Tool.copy(level1.value) || [];
<!--treeSelectData.value = Tool.copy(level1.value);-->
treeSelectData.value = Tool.copy(level1.value) || [];
Comments | NOTHING