文章目录
shopping-project-商品列表显示及查看商品详情
1 前后端环境的搭建
1.1 设及技术
- springBoot
- mybatis
- Redis
- JWT
- springSecurity
- Vue+Element+Axios
…
1.2 前后端分离
1.2.1 服务端项目
-
myshopping-project-server
-
在IDEA创建springboot项目,添加相关依赖包
-
配置application.yml
#配置服务器 server: port: 80 servlet: context-path: / spring: #配置数据源 datasource: url: jdbc:mysql://localhost:3306/myshopping?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root #配置数据库连接池 druid: max-active: 100 min-idle: 10 initial-size: 10 max-wait: 1000 #配置日志 logging: level: root: info web: trace #mybatis配置 mybatis: configuration: #日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #映射文件路径 mapper-locations: /mapper/*Mapper.xml
-
写一个controller进行测试,运行
成功
-
在核心配置类进行跨域配置
@Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { /** * 进行跨域配置 */ @Override public void addCorsMappings(CorsRegistry registry) { registry .addMapping("/**")//当前应用所有资源可被访问 .allowedOrigins("http://localhost:8080")//设置允许访问当前应用的项目 .allowedMethods("*") //设置允许哪些请求方式进行跨域访问,默认情况只允许简单请求(get,post,head) .allowCredentials(true);//是否允许使用凭证,是否允许使用session后cookie存储凭证信息 } }; }
-
1.2.2 客户端项目
-
myshopping-project-client
-
使用vue-cli构建项目
-
配置index.html页面布局
<style> html { width: 100%; height: 100%; } body { width: 100%; height: 100%; margin: 0px; } </style>
-
App.vue样式
#app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; width: 100%; height: 100%; color: #2c3e50; }
-
关闭语法检测
const { defineConfig } = require("@vue/cli-service"); module.exports = defineConfig({ transpileDependencies: true, lintOnSave:false });
-
测试运行
成功…
-
配置axios
- npm引入
cnpm intall axios
- 在main.js配置 axios
import axios from "axios"; //设置axios是Vue的属性 Vue.prototype.$axios = axios.create({ baseURL: 'https://localhost/',//设置axios请求的基础路径 headers: { 'X-Requested-With': 'XMLHttpRequest' },//指定axios的请求为ajax请求 withCredentials: true, // 跨域请求是否允许使用凭证 });
- 测试使用
<template> <div> <button @click="test">测试</button> </div> </template> <script> export default{ methods:{ test(){ this.$axios.get('test/test01') .then(response=>{ alert(response.data); }) .catch(error=>{ alert(error) }); }, } } </script> <style scoped> </style>
-
配置ElementUI
-
npm引入
npm i element-ui -S
-
在mian.js配置ElementUI
import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI);//设置Vue使用Element组件
-
测试
<el-button type="primary" @click="test">测试</el-button>
-
-
2 查看商品列表
2.1 服务端
2.1.1 创建商品model(此处仅有图书,后续会加入其他)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
private Integer book_id;
private String book_name;
private Double book_price;
private String book_image;
private String book_desc;
private Integer book_is_display;
}
2.1.2 创建BookController
@RestController
@RequestMapping("/book")
public class BookController {
@Resource
private BookService bookService;
@RequestMapping("/queryBooks")
public List<Book> queryBook(){
return bookService.queryBook();
}
}
2.1.3 创建BookServcie
- BookService接口
public interface BookService {
/**
* 获得所有图书,
* @param isDisplay 表示是否上架 0表示上架 -1表示下架,没有参数表示所有商品
* @return
*/
List<Book> queryBook(Integer... isDisplay);
}
- BookServcieImpl
@Service
public class BookServiceImp implements BookService {
@Resource
private BookMapper bookMapper;
@Override
public List<Book> queryBook(Integer... isDisplay) {
return bookMapper.queryBook(isDisplay);
}
}
2.1.4 创建BookMapper
- BookMapper接口
@Repository
public interface BookMapper {
List<Book> queryBook(Integer... isDisplay);
}
- BookMapper.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.shopping.mapper.BookMapper">
<select id="queryBook" resultType="com.shopping.model.Book">
select * from myshopping.tbl_book
<if test="isDisplay!=null and isDisplay.length!=0">
<where>
book_is_display=#{isDisplay[0]}
</where>
</if>
</select>
</mapper>
2.1.5 Postman测试接受数据是否成功
成功
2.2 客户端
2.2.1 接收后端传来的数据及某些数据做出的相应处理
export default {
data(){
return{
booksList:[],//商品数组
num:1,//购买数量
}
},
methods: {
/**
* 获得商品信息
*/
queryBooks(){
this.$axios
.get("book/queryBooks")
.then(response=>{
this.booksList =response.data;
})
.catch(err=>{
alert(err)
})
},
/**
* 截取书名,要求书名必须小于10
* @param {} bookName
*/
subBookName(bookName){
if(bookName.length>=10){
bookName=bookName.substring(0,10)+"...";
}
return bookName;
}
},
created(){
this.queryBooks();
}
}
2.2.2 前端模板部分及美化
<template>
<div class="index_container">
<el-container>
<el-header>Header</el-header>
<el-main>
<el-row>
<el-col :span="6" v-for="(book,index) in booksList" :key="book.book_id" :offset="index==0?0:0" >
<el-card :body-style="{ padding: '0px'}" style="margin-left: 10px; margin-top: 20px;">
<img :src="require('@/assets/images/books/'+book.book_image)" class="image">
<div class="bottom">
<div style="display: flex;">
<div class="book_desc" >
<div>
<el-tooltip class="item" effect="dark" :content="book.book_name" placement="top-start">
<span>书名:{{ subBookName(book.book_name)}}</span>
</el-tooltip>
</div>
<div><span>单价:{{book.book_price}}¥</span></div>
</div>
<div class="book_car" >
<i style="color: orange;font-size:40px;" class=el-icon-shopping-cart-full></i>
</div>
</div>
<div class="button_inputnumber" style="display: flex;">
<div style="margin-top: 5px;"><el-input-number size="small" v-model="num" :min="1"></el-input-number></div>
<div><el-button type="warning" >购买</el-button></div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer>Footer</el-footer>
</el-container>
</div>
</template>
<style scoped>
.index_container{
margin: 0px auto;
width: 80%;
}
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-main {
background-color: #E9EEF3;
color: #333;
}
.book_desc{
font-size: 13px;
width:70%;
}
.book_car{
width:30%;
margin-top: 5px;
}
.book_desc div{
margin-left: 10px;
margin-top: 10px;
}
.button_inputnumber div{
margin: auto;
}
.bottom {
margin-top: 13px;
line-height: 12px;
}
.image {
height: 300px;
width: 100%;
display: block;
}
</style>
3 查看商品详情
3.1 客户端
3.1.1 给商品名路由链接,并配置路由
Vue没有超链接
<a herf="">
标签不能使用,需使用路由连接<router-link to="">
<router-link class="normal" to="/bookDesc">书名:{{ subBookName(book.book_name) }} </router-link>
- 配置路由
import BookDesc from "../views/BookDesc.vue"
const routes = [
//配置图书详情页的路由
{
path: "/bookDesc",
name: "bookDesc",
component: BookDesc
},
];
- 美化路由链接
.normal{
display: block;
color: black;
text-decoration: none;
}
.normal:hover{
color: grey;
}
.normal:active{
color: black;
}
3.1.2 在BookDesc.vue写商品详情页的前端模板及其美化
<template>
<div class="index_container">
<el-container>
<el-header>Header</el-header>
<el-main>
<div class="desc-div">
<h2 align="center">商品详情</h2>
<div class="goodsDesc">
<div class="baseInfo">
<div class="photo">
<el-image :src="require('@/assets/images/books/' + book.book_image)">
<div slot="placeholder" class="image-slot">
加载中<span class="dot">...</span>
</div>
</el-image>
</div>
<div class="bookInfo">
<p>书名:{{book.book_name}}</p>
<p>价格:{{book.book_price}}</p>
</div>
</div>
<div class="bookDesc">
<p>图书详情:{{ book.book_desc}}</p>
</div>
<div class="book-comment">
<p>图书评论</p>
<div class="comment-info" >
<div class="comment-base-info">
<label style="display:inline-block;width:70%">评论人:</label>
<label style="margin-right:20px">评论时间:</label>
</div>
<div class="comment-content">
</div>
</div>
</div>
</div>
</div>
</el-main>
<el-footer>Footer</el-footer>
</el-container>
</div>
</template>
<style scoped>
.index_container {
margin: 0px auto;
width: 80%;
}
.el-header,.el-footer {
background-color: #b3c0d1;
color: #333;
text-align: center;
line-height: 60px;
}
.el-header{
display: flex;
}
.el-header .index-logo,.el-header .index-personal{
width:50%
}
.el-main {
background-color: #fff;
color: #333;
}
.desc-div{
width:100%;
border: 0px solid red;
}
.goodsDesc{
width: 80%;
margin:0 auto;
border:1px solid blue;
}
.baseInfo{
display: flex;
}
.baseInfo .photo{
width: 50%;
text-align: center;
margin-bottom:10px;
}
.bookDesc>p{
width:100%;
height: 40px;
line-height: 40px;
padding-left:10px;
box-sizing:border-box;
color: #fff;
background-color:darkgray;
}
.book-comment>p{
width:100%;
height: 40px;
line-height: 40px;
padding-left:10px;
box-sizing:border-box;
color: #fff;
background-color:darkgray;
margin-bottom: 0px;
}
.comment-base-info{
width:100%;
height: 40px;
line-height: 40px;
padding-left:10px;
box-sizing:border-box;
color: #fff;
background-color:yellowgreen
}
.comment-content{
padding:15px 10px;
}
</style>
3.1.3 配置路由传参
具体问题详见:VUE中路由跳转传参问题
-
通过query传递参数
<router-link class="normal" :to="{path:'bookDesc',query:{'book':book}}"> 书名:{{ subBookName(book.book_name) }} </router-link>
-
接受参数
created() { let book=this.$route.query.book; }
3.1.4 接收后端传来的参数并做出处理
export default {
data() {
return {
book:{},
}
},
methods: {
//根据id查询书的信息
queryBookById(book_id){
this.$axios
.get("/book/queryBookById")
.then(response=>{
this.book=response.data;
})
.catch(error=>{
alert(error)
});
}
},
created() {
//获得Index.vue传来的值
let book_id=this.$route.query.book_id;
this.queryBookById(book_id)
}
}
3.2 服务端
3.2.1 BookController里增加该功能
@RequestMapping("/queryBookById")
public Book queryBookById(Integer book_id){
return bookService.queryBookById(book_id);
}
3.2.2 BookService增加该功能
- BookService接口
/**
* 根据id查询图书信息
* @param book_id
* @return
*/
Book queryBookById(Integer book_id);
- BookService接口实现
@Override
public Book queryBookById(Integer book_id) {
return bookMapper.queryBookById(book_id).get(0);
}
3.2.3 BookMapper增加该功能
- BookMapper接口
List<Book> queryBookById(Integer... book_ids);
- BookMapper.xml
<select id="queryBookById" resultType="com.shopping.model.Book">
select * from myshopping.tbl_book
where book_id in
<foreach open="(" collection="book_ids" item="book_id" close=")" separator=",">
#{book_id}
</foreach>
</select>
4 查看图书评论信息
4.1 服务端
4.1.1 创建comment的model
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommentView implements Serializable {
private Integer comment_id;
private Integer user_id;
private Integer book_id;
private String comment_message;
private Date comment_time;
}
4.1.2 BookController里增加该功能
@RequestMapping("/queryCommentById")
public List<CommentView> queryCommentById(Integer book_id){
return bookService.queryCommentById(book_id);
}
4.1.3 BookService增加该功能
- BookService接口
/**
* 根据id查询图书评论
* @param book_id
* @return
*/
List<CommentView> queryCommentById(Integer book_id);
- BookService接口实现
@Resource
private CommentMapper commentMapper;
@Override
public List<CommentView> queryCommentById(Integer book_id) {
return commentMapper.queryCommentById(book_id);
}
4.1.4 创建CommentMapper
- CommentMapper接口
@Repository
public interface CommentMapper {
List<CommentView> queryCommentById(Integer book_id);
}
- CommentMapper.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.shopping.mapper.CommentMapper">
<select id="queryCommentById" resultType="com.shopping.model.view.CommentView">
select tbl_comment.comment_id,
tbl_comment.comment_message,
tbl_comment.comment_time,
user.user_name
from myshopping.tbl_comment join myshopping.tbl_user user
on tbl_comment.user_id = user.user_id
where tbl_comment.book_id=#{book_id}
</select>
</mapper>
4.2 客户端
4.2.1 接收后端传来的数据,并做出处理
export default {
data() {
return {
commentList:[], //图书评论
}
},
methods: {
/**
* 根据图书编号查询评论
* @param {} book_id
*/
queryCommentById(book_id){
this.$axios
.get("/book/queryCommentById?book_id=" + book_id)
.then(response => {
this.commentList = response.data;
})
.catch(error => {
alert(error)
});
}
},
created() {
let book_id = this.$route.query.book_id;
this.queryCommentById(book_id);
}
}
4.2.2 前端显示
<div class="book-comment">
<p>图书评论</p>
<div class="comment-info" v-for="comment in commentList" :key="comment.comment_id">
<div class="comment-base-info">
<label style="display:inline-block;width:60%">评论人:{{comment.user_name}} </label>
<label style="margin-right:20px">评论时间:{{comment.comment_time}}</label>
</div>
<div class="comment-content">{{ comment.comment_message }}</div>
</div>
<div class="comment-info" v-if="commentList.length==0">
<div class="comment-content">暂无评论</div>
</div>
</div>
5 对商品评论进行分页处理
- 基于程序分页(不用):一次性将数据查出,在程序进行逻辑处理实现分页
- 基于数据库分页:根据分页的要求,一次查出一页数据mysql
limit
sqlservertop
oraclerowNum
5.1 服务端
5.1.1添加pagehelper依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
5.1.2 配置PageHelper
pagehelper:
#配置pagehelper方言,不同数据库生成sql语句不同
helper-dialect: mysql
#合理化配置,配置后访问的页码不存在则自动跳转到第一页
reasonable: true
#自动分页的配置,依据的是入参,如果参数中有pageNum,pageSize分页参数,则会自动分页.
supportMethodsArguments: true
5.1.3 创建公共类-分页参数类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageParams {
private Integer pageSize;//每页记录数量
private Integer pageNum;//当前页
}
5.1.4 Service层写分页代码-BookServiceImpl类
public Map<String, Object> queryCommentById(Integer book_id, PageParams pageParams) {
//使用PageHelper实现分页
//开始分页插件
PageHelper.startPage(pageParams);
List<CommentView> commentViews = commentMapper.queryCommentById(book_id);
//将查询结果封装到PageInfo对象中,该对象是PageHelper提供的
//该对象会根据commentViews来封装分页相关数据
PageInfo<CommentView> pageInfo=new PageInfo<>(commentViews);
//将分页需要返回的客户端数据封装到Map结合中
Map<String,Object> map=new HashMap<>();
map.put("total",pageInfo.getTotal());
map.put("commentList",pageInfo.getList());
// System.out.println(map.get("total")+"///"+map.get("commentList"));
return map;
}
5.1.5 修改BookController
@RequestMapping("/queryCommentById")
public Map<String, Object> queryCommentById(Integer book_id, PageParams pageParams){
return bookService.queryCommentById(book_id,pageParams);
}
5.2 客户端
5.2.1 前端使用Element的分页插件
<div class="pagination">
<el-pagination background :page-size="pageParams.pageSize"
:current-page.sync="pageParams.pageNum"
layout="prev, pager, next,jumper,->,total"
:total="total"
@current-change="queryCommentByBookId(book.book_id)">
</el-pagination>
</div>
5.2.2 前端定义分页参数,及向后端传递参数
export default {
data() {
return {
commentList: [],//图书评论数组
/**
* 分页参数
*/
pageParams: {
pageSize: 4,//每页显示的行数
pageNum: 1//设置当前页
},
total: 0,//总行数
}
},
methods: {
/**
* 根据图书编号获得该图书的所有评论信息
*/
queryCommentByBookId(book_id) {
this.pageParams.book_id = book_id;//向对象中添加了一个属性
this.$axios
.get('book/queryCommentById', {
params: this.pageParams
})
.then(response => {
let map = response.data;
this.total = map.total;//获得总记录数
this.commentList = map.commentList;//获得查询的具体数据
})
.catch(error => {
alert(error);
})
}
},
created() {
//获得Index.vue传入的查询参数
let book_id = this.$route.query.book_id;
this.queryCommentByBookId(book_id);
}
}
6 查看商品列表-基于Redis查询
- 前端第一次获取数据,后端程序访问Redis,检测有无数据,若无数据,后端程序从mysql中获取数据,mysql返回的数据,后端程序将数据一份保存在Redis中,一份在显示在前端
- 第二次访问Redis,里面包含数据,后端程序直接从Redis中返回数据到前端
-
数据库与Redis同步问题
- 当数据库中的数据发生改变直接删除Redis中的数据
- 当数据库中某条数据发生改变,修改Redis中该条数据
…
6.1 服务端
6.1.1引入Redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${springboot-date-redis}</version>
</dependency>
6.1.2 在Service层做处理-BookServiceImpl
-
思路
-
检测商品是否为上架商品
①如果传来的数据为0,则表示上架商品,默认从Redis中读取
②若传来数据不为0,则从数据库中读取
-
检测Redis是否包含上架商品
①若包含直接Redis中读取
②若不包含则从数据库中读取以上架商品
-
从数据库中获取已上架数据并向Redis中存储一份,然后返回
-
-
采用hash存储商品信息显然更合适
-
注入
RedisTemplate
private RedisTemplate redisTemplate;
//构造器注入,并设置序列化Redis
@Autowired
public BookServiceImpl(BookMapper bookMapper, CommentMapper commentMapper, RedisTemplate redisTemplate) {
this.bookMapper = bookMapper;
this.commentMapper = commentMapper;
this.redisTemplate = redisTemplate;
this.redisTemplate.setKeySerializer(new StringRedisSerializer());
this.redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
}
- 具体业务代码
@Override
public List<Book> queryBook(Integer... isDisplay) {
//从数据库获取数据
List<Book> bookList = bookMapper.queryBook(isDisplay);
Map<String,Object> booksMap=new HashMap<>();
//检测商品是否上架
if (isDisplay[0] == 0) {
//获得hash操作对象
HashOperations hashOperations = redisTemplate.opsForHash();
//获得Redis中bookList中的数据
Map bookListMap = hashOperations.entries("bookList");
//检测Redis中是否存在数据,如不存在从数据库中获取数据
if (bookListMap.size() == 0) {
//遍历bookList,将bookList中数据转存到booksMap集合中
for (Book book:bookList){
booksMap.put("BOOKKEY:"+book.getBook_id(), book);
}
//将booksMap集合存到Redis中,RedisKey为BookList
hashOperations.putAll("bookList",booksMap);
return bookList;
}else {//否则从Redis中直接获取数据
Collection<Book> values = bookListMap.values();
List<Book> bookLists=new ArrayList<>();
for (Book book:values){
bookLists.add(book);
}
return bookLists;
}
}else{
return bookList;
}
}