fmmall
本文最后更新于:9 个月前
商城项目
项目地址:https://github.com/sunzhengyu99/fmmall/tree/master
1.业务逻辑开发
1.1登录注册
1.1.1 完成dao操作
创建实体类
package com.sunzy.fmmall.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.sql.Date; @Data @AllArgsConstructor @NoArgsConstructor public class Users { private Integer userId; private String username; private String password; private String nickname; private String realname; private String userImg; private String userMobile; private String userEmail; private String userSex; private Date userBirth; private Date userRegtime; private Date userModtime; }
编写UserDao文件
package com.sunzy.fmmall.dao; import com.sunzy.fmmall.entity.User; import com.sunzy.fmmall.entity.Users; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserDao { public User queryUserByName(String name); public User insertUser(Users user); }
编写UserMapper.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.sunzy.fmmall.dao.UserDao"> <resultMap id="BaseResultMap" type="com.sunzy.fmmall.entity.Users"> <!-- WARNING - @mbg.generated --> <id column="user_id" jdbcType="INTEGER" property="userId" /> <result column="username" jdbcType="VARCHAR" property="username" /> <result column="password" jdbcType="VARCHAR" property="password" /> <result column="nickname" jdbcType="VARCHAR" property="nickname" /> <result column="realname" jdbcType="VARCHAR" property="realname" /> <result column="user_img" jdbcType="VARCHAR" property="userImg" /> <result column="user_mobile" jdbcType="VARCHAR" property="userMobile" /> <result column="user_email" jdbcType="VARCHAR" property="userEmail" /> <result column="user_sex" jdbcType="CHAR" property="userSex" /> <result column="user_birth" jdbcType="DATE" property="userBirth" /> <result column="user_regtime" jdbcType="TIMESTAMP" property="userRegtime" /> <result column="user_modtime" jdbcType="TIMESTAMP" property="userModtime" /> </resultMap> <insert id="insertUser"> insert into users(username, password, user_img, user_regtime, user_modtime) values(#{username}, #{password},#{userImg}, #{userRegtime},#{userModtime}) </insert> <select id="queryUserByName" resultMap="BaseResultMap"> select user_id, username, password, nickname, realname, user_img, user_mobile, user_email, user_sex, user_birth, user_regtime, user_modtime from users where username = #{name} </select> </mapper>
1.1.2 完成service功能
创建接口
com.sunzy.fmmall.service.UserService
package com.sunzy.fmmall.service; import com.sunzy.fmmall.vo.ResultVO; public interface UserService { public ResultVO login(String username, String password); public ResultVO regist(String username, String password); }
创建实现类
com.sunzy.fmmall.service.Impl.UserServiceImpl
package com.sunzy.fmmall.service.Impl; import com.sunzy.fmmall.dao.UserDao; import com.sunzy.fmmall.entity.Users; import com.sunzy.fmmall.service.UserService; import com.sunzy.fmmall.utils.MD5Utils; import com.sunzy.fmmall.vo.ResStatus; import com.sunzy.fmmall.vo.ResultVO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.Date; @Service public class UserServiceImpl implements UserService { @Resource private UserDao userDao; @Override public ResultVO login(String username, String password) { Users user = userDao.queryUserByName(username); if(user == null){ return new ResultVO(ResStatus.NO, "用户不存在!", null); } String realPasswd = user.getPassword(); if(realPasswd.equals(MD5Utils.md5(password))){ return new ResultVO(ResStatus.OK, "登录成功!", user); }else { return new ResultVO(ResStatus.NO, "密码错误!", null); } } @Transactional @Override public ResultVO regist(String username, String password) { synchronized (this){ // 判断用户是否已经注册 Users user = userDao.queryUserByName(username); if(user == null){ // 将新用户信息添加到数据库中 String md5 = MD5Utils.md5(password); user = new Users(); user.setPassword(md5); user.setUsername(username); user.setUserImg("img/default.png"); user.setUserRegtime(new Date()); user.setUserModtime(new Date()); int i = userDao.insertUser(user); if(i > 0){ return new ResultVO(ResStatus.OK, "用户注册成功!", user); }else { return new ResultVO(ResStatus.NO, "用户注册失败!", null); } }else{ return new ResultVO(ResStatus.NO, "用户已被注册!", null); } } } }
1.1.3 完成controller代码
package com.sunzy.fmmall.controller;
import com.sunzy.fmmall.entity.Users;
import com.sunzy.fmmall.service.UserService;
import com.sunzy.fmmall.vo.ResultVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
//@RequestBody
@RestController
@RequestMapping("/user")
@Api(value = "用户管理", tags = "提供用户注册和登录接口")
public class UserController {
@Autowired
private UserService userService;
@ApiOperation("用户登录接口")
@ApiImplicitParams({
@ApiImplicitParam(dataType = "string", name = "username", value = "用户账号", required = true),
@ApiImplicitParam(dataType = "string", name = "password", value = "用户密码", required = true)
})
@GetMapping("/login")
public ResultVO login(@RequestParam("username") String username,
@RequestParam("password") String password){
return userService.login(username, password);
}
@ApiOperation("用户注册接口")
// @ApiImplicitParams({
// @ApiImplicitParam(dataType = "string", name = "username", value = "用户注册账号", required = true),
// @ApiImplicitParam(dataType = "string", name = "password", value = "用户注册密码", required = true)
// })
@PostMapping("/regist")
public ResultVO regist(@RequestBody Users user){
String username = user.getUsername();
String password = user.getPassword();
return userService.regist(username, password);
}
}
1.1.4 接口测试
1.2 解决前后端跨域问题
1.1 前端
通过jsonp跨域
通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
1.2 后端
只需要在controller中添加@CrossOrigin
注解即可允许后端响应数据进行跨域响应。
1.3 将用户信息显示在前端
在前端登录(login.html的doSubmit方法中)的ajax请求的响应代码中加入以下内容
if(vo.code == 10000){
//如果登录成功,就把token存储到cookie
setCookieValue("token",vo.msg);
//将用户昵称和用户头像的路径保存在cookie
setCookieValue("userId",vo.data.userId);
setCookieValue("username",vo.data.username);
setCookieValue("userImg",vo.data.userImg)
}
即登录成功后,将用户的token和用户信息一同保存到cookie中,用于前端传值
在index.html的created函数中添加以下代码
var token = getCookieValue("token");
if(token !=null && token !=""){
this.isLogin = true;
this.username = getCookieValue("username");
this.userimg = getCookieValue("userImg");
}
获取到用户的基本信息,再通过v-model显示到前端页面中。
1.4 首页轮播图
数据库操作实现
编写sql语句
SELECT * FROM index_img WHERE `status` = 1 ORDER BY seq
在IndexImgMapper定义方法
public interface IndexImgMapper extends GeneralDao<IndexImg> { public List<IndexImg> listIndexImgs(); }
配置映射文件
<select id="listIndexImgs" resultType="com.sunzy.fmmall.entity.IndexImg" resultMap="BaseResultMap"> SELECT * FROM index_img WHERE `status` = 1 ORDER BY seq </select>
业务逻辑实现
IndexImgServiceImpl添加以下内容
@Service public class IndexImgServiceImpl implements IndexImgService { @Resource private IndexImgMapper indexImgMapper; @Override public ResultVO listIndeximgs() { List<IndexImg> indexImgs = indexImgMapper.listIndexImgs(); if(indexImgs.size() == 0){ return new ResultVO(ResStatus.NO, "failed", null); } return new ResultVO(ResStatus.OK, "success",indexImgs); } }
接口实现
IndexImgController
@RestController @RequestMapping("/index") @CrossOrigin public class IndexImgsController { @Autowired private IndexImgService indexImgService; @GetMapping("/indeximg") public ResultVO addGoods(){ return ResultVO.success(indexImgService.listIndeximgs()); } }
1.5 分类列表
接口开发
数据库分析
添加实体类CategoryVO
与category的区别在于多了一个属性用于存放子标题
List<CategoryVO> categoryVOList;
dao实现
方法一
使用递归sql查询数据库
dao代码
public List<CategoryVO> getCategoryList2(int parentId);
mapper映射文件
<resultMap id="ResultMap" type="com.sunzy.fmmall.entity.CategoryVO"> <!-- WARNING - @mbg.generated --> <id column="category_id" jdbcType="INTEGER" property="categoryId" /> <result column="category_name" jdbcType="VARCHAR" property="categoryName" /> <result column="category_level" jdbcType="INTEGER" property="categoryLevel" /> <result column="parent_id" jdbcType="INTEGER" property="parentId" /> <result column="category_icon" jdbcType="VARCHAR" property="categoryIcon" /> <result column="category_slogan" jdbcType="VARCHAR" property="categorySlogan" /> <result column="category_pic" jdbcType="VARCHAR" property="categoryPic" /> <result column="category_bg_color" jdbcType="VARCHAR" property="categoryBgColor" /> <collection property="categoryVOList" column="category_id" select="com.sunzy.fmmall.dao.CategoryMapper.getCategoryList2"/> </resultMap> <select id="getCategoryList2" resultType="com.sunzy.fmmall.entity.CategoryVO" resultMap="ResultMap"> select * from category where parent_id = #{parentId} </select>
方法二
直接获取所有分类的数据后,在java代码中进行级别分类的处理,可以大大提高处理效率
service代码实现
方法一
@Override public List<CategoryVO> getCategoryList() { return categoryMapper.getCategoryList2(0); }
方法二
@Override public List<CategoryVO> getCategoryList() { List<Category> categories = categoryMapper.getCategoryList(); List<CategoryVO> categoryVOS = new ArrayList<>(); // 获取所有的一级标题 for (Category category : categories) { CategoryVO categoryVO = new CategoryVO(); if(category.getCategoryLevel() == 1){ BeanUtils.copyProperties(category, categoryVO); categoryVOS.add(categoryVO); } } // 将一级标题下的二级标题添加到setCategoryVOList属性中 for (CategoryVO categoryVO : categoryVOS) { List<CategoryVO> category2List = new ArrayList<>(); for (Category category : categories) { // 筛选条件为 二级标题且父标题与一级标题一致 if(category.getCategoryLevel() == 2 && category.getParentId().equals(categoryVO.getCategoryId())){ CategoryVO categoryvo = new CategoryVO(); BeanUtils.copyProperties(category, categoryvo); category2List.add(categoryvo); } } categoryVO.setCategoryVOList(category2List); // System.out.println(categoryVO); } for (CategoryVO categoryVO : categoryVOS) { for(CategoryVO categoryVO2: categoryVO.getCategoryVOList()){ //遍历所有二级标题 List<CategoryVO> category3List = new ArrayList<>(); //保存三级标题的数组 for (Category category : categories) { // 筛选符合条件的三级标题 存放到二级标题的 categoryVOList中 if(category.getCategoryLevel() == 3 && category.getParentId().equals(categoryVO2.getCategoryId())){ CategoryVO categoryvo = new CategoryVO(); BeanUtils.copyProperties(category, categoryvo); category3List.add(categoryvo); } } categoryVO2.setCategoryVOList(category3List); } System.out.println(categoryVO); } return categoryVOS; }
controller实现
@Autowired private CategoryService categoryService; @GetMapping("/category-list") @ApiOperation(value = "获取首页分类数据") public ResultVO getCategoryList(){ return ResultVO.success(categoryService.getCategoryList1()); // return ResultVO.success(categoryService.getCategoryList2()); }
实测方法二比方法一的响应速度要快很多倍,所以还是减少在数据库中进行数据的处理。
方法一 方法二 实现效果如下
1.6 商品推荐
推荐商品原则可以是 1.根据商城销量推荐2.推荐商城最新上架的商品
说明:商品推荐算法是根据多个维度进行权重计算的结果,计算出一个匹配值
数据库操作
select * from product order by create_time desc limit 0,3;
添加实体类ProdoctVO
与product的区别在于多一个属性用于存在与该产品相关的图片
private List<ProductImg> imgs; public List<ProductImg> getImgs() { return imgs; } public void setImgs(List<ProductImg> imgs) { this.imgs = imgs; }
dao实现
ProductDao
@Mapper public interface ProductMapper extends GeneralDao<Product> { public List<Product> getRecommendProduct(); }
xml
<id column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="category_id" jdbcType="INTEGER" property="categoryId" /> <result column="root_category_id" jdbcType="INTEGER" property="rootCategoryId" /> <result column="sold_num" jdbcType="INTEGER" property="soldNum" /> <result column="product_status" jdbcType="INTEGER" property="productStatus" /> <result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> <result column="content" jdbcType="LONGVARCHAR" property="content" /> </resultMap> <select id="getRecommendProduct" resultType="com.sunzy.fmmall.entity.Product" resultMap="BaseResultMap"> select * from product order by create_time desc limit 0,3; </select>
ProductImgDao
@Mapper public interface ProductImgMapper extends GeneralDao<ProductImg> { public List<ProductImg> getProductImgsById(int productId); }
xml
<id column="id" jdbcType="VARCHAR" property="id" /> <result column="item_id" jdbcType="VARCHAR" property="itemId" /> <result column="url" jdbcType="VARCHAR" property="url" /> <result column="sort" jdbcType="INTEGER" property="sort" /> <result column="is_main" jdbcType="INTEGER" property="isMain" /> <result column="created_time" jdbcType="TIMESTAMP" property="createdTime" /> <result column="updated_time" jdbcType="TIMESTAMP" property="updatedTime" /> </resultMap> <select id="getProductImgsById" resultType="com.sunzy.fmmall.entity.ProductImg" resultMap="BaseResultMap"> select * from product_img where item_id = #{productId} </select>
service代码
@Resource private ProductMapper productMapper; @Resource private ProductImgMapper productImgMapper; @Override public List<ProductVO> getRecommendProductList() { List<Product> recommendProductList = productMapper.getRecommendProduct(); List<ProductVO> productVOS = new ArrayList<>(); for (Product product : recommendProductList) { ProductVO productVO = new ProductVO(); List<ProductImg> imgs = productImgMapper.getProductImgsById(Integer.parseInt(product.getProductId())); BeanUtils.copyProperties(product, productVO); productVO.setImgs(imgs); productVOS.add(productVO); } return productVOS; }
controller代码
@Autowired private ProductService productService; @GetMapping("/list-recommends") @ApiOperation(value = "获取商品推荐数据") public ResultVO getRecommendsList(){ return ResultVO.success(productService.getRecommendProductList()); }
测试结果
1.7 分类商品推荐
一次性加载所有分类的推荐商品,整体完成初始化
分别获取所有一级标题下销量最高的六个商品
返回查询到的6个商品
sql语句
from product where root_category_id = rootId and product_status = 1 order by sold_num desc limit 0,6;
获取到rootId下销量前六的商品信息
dao
public List<Product> getRecommendByCategory(int rootId);
xml
<select id="getRecommendByCategory" resultType="com.sunzy.fmmall.entity.Product" resultMap="BaseResultMap"> select * from product where root_category_id = #{rootId} and product_status = 1 order by sold_num desc limit 0,6; </select>
service
@Override public List<CategoryDTO> getRecommendByCategory() { List<Category> categoryList = categoryMapper.getCategoryList(); // 获取所有的分类 List<CategoryDTO> categoryDTOList = new ArrayList<>(); // 用于保存结果 for (Category category : categoryList) { if (category.getCategoryLevel() == 1) { CategoryDTO categoryDTO = new CategoryDTO(); List<ProductVO> productVOList = new ArrayList<>(); List<Product> productList = productMapper.getRecommendByCategory(category.getCategoryId()); // 根据root_category_id筛选属于该一级分类的商品 for (Product product : productList) { // 将产品的图片查询出来加入到productVO的imgs属性中 ProductVO productVO = new ProductVO(); List<ProductImg> imgs = productImgMapper.getProductImgsById(Integer.parseInt(product.getProductId())); BeanUtils.copyProperties(product, productVO); productVO.setImgs(imgs); productVOList.add(productVO); } // 将处理得到的productVOList加入到CategoryDTO的products属性中 categoryDTO.setProducts(productVOList); BeanUtils.copyProperties(category, categoryDTO); categoryDTOList.add(categoryDTO); } } return categoryDTOList; }
** 该方法实现的有点复杂,并且效率比较低,可以采用联合查询的方法进行优化
controller
@GetMapping("/category-recommends") @ApiOperation(value = "根据商品分类销量前六推荐商品") public ResultVO getRecommendByCategoryList(){ return ResultVO.success(productService.getRecommendByCategory()); }
接口测试
前端实现效果
1.8 商品详情
点击首页推荐的商品、轮播图商品广告、商品列表页面点击商品,就会进入到商品的详情页面
用户点击时,携带商品的id进行后端请求,后端接收到商品id后,进行数据库查询,返回详细信息
包括以下内容
1.商品的基本信息
2.商品的套餐信息
3.商品的图片信息
4.商品的评价信息
5.商品的参数信息
商品详情接口
接口所需信息如下,只需要三个单表查询即可完成,因此dao层可以直接使用tkMapper提供的接口
service
public ResultVO getProductBasicInfo(String productId) { //1.商品基本信息 Example example = new Example(Product.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("productId",productId); criteria.andEqualTo("productStatus",1);//状态为1表示上架商品 List<Product> products = productMapper.selectByExample(example); if(products.size() > 0){ Example exampleImg = new Example(ProductImg.class); Example.Criteria imgCriteria = exampleImg.createCriteria(); imgCriteria.andEqualTo("itemId",productId); List<ProductImg> productImgs = productImgMapper.selectByExample(exampleImg); Example exampleSku = new Example(ProductSku.class); Example.Criteria skuCriteria = exampleSku.createCriteria(); skuCriteria.andEqualTo("productId",productId); List<ProductSku> productSkus = productSkuMapper.selectByExample(exampleSku); HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("product", products.get(0)); hashMap.put("productImgs", productImgs); hashMap.put("productSkus", productSkus); return ResultVO.success(hashMap); } return ResultVO.failed("查询的商品不存在"); }
controller
@RestController @CrossOrigin @RequestMapping("/product") @Api(value = "提供商品信息相关的接口",tags = "商品管理") public class ProductController { @Autowired private ProductService productService; @Autowired private ProductCommontsService productCommontsService; @ApiOperation("商品基本信息查询接口") @GetMapping("/detail-info/{pid}") public ResultVO getProductBasicInfo(@PathVariable("pid") String pid){ return productService.getProductBasicInfo(pid); } }
接口测试
商品参数接口
接口所需数据可知,也为单表查询,因此dao层可以直接使用tkMapper提供的接口
service
@Override public ResultVO getProductParamsById(String productId) { //1.商品基本信息 Example example = new Example(Product.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("productId",productId); criteria.andEqualTo("productStatus",1);//状态为1表示上架商品 List<Product> products = productMapper.selectByExample(example); if(products.size() > 0){ Example exampleParams = new Example(ProductParams.class); Example.Criteria criteria1 = exampleParams.createCriteria(); criteria1.andEqualTo("productId", productId); List<ProductParams> params = productParamsMapper.selectByExample(exampleParams); if(params.size() > 0){ return ResultVO.success(params.get(0)); }else { return ResultVO.failed("此产品为三无产品!"); } }else { return ResultVO.failed("查询的商品不存在"); } }
controller
@ApiOperation("商品参数信息查询接口") @GetMapping("/detail-params/{pid}") public ResultVO getProductParams(@PathVariable("pid") String pid){ return productService.getProductParamsById(pid); }
测试结果
商品评价接口
评价接口需要完成两个功能:评价列表分页展示和评价分析
评价列表分页展示
接口所需参数如上图,可以看出需要关联用户数据,因此为多表关联查询
sql
select u.username, u.nickname, u.user_img, c.comm_id, c.product_id, c.product_name, c.order_item_id, c.user_id, c.is_anonymous, c.comm_type, c.comm_level, c.comm_content, c.comm_imgs, c.sepc_name, c.reply_status, c.reply_content, c.reply_time, c.is_show from product_comments c INNER JOIN users u ON u.user_id = c.user_id WHERE c.product_id = 3 limit 1,5
dao
<resultMap id="ProductCommentsVOMap" type="com.qfedu.fmmall.entity.ProductCommentsVO"> <id column="comm_id" jdbcType="VARCHAR" property="commId" /> <result column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="order_item_id" jdbcType="VARCHAR" property="orderItemId" /> <result column="is_anonymous" jdbcType="INTEGER" property="isAnonymous" /> <result column="comm_type" jdbcType="INTEGER" property="commType" /> <result column="comm_level" jdbcType="INTEGER" property="commLevel" /> <result column="comm_content" jdbcType="VARCHAR" property="commContent" /> <result column="comm_imgs" jdbcType="VARCHAR" property="commImgs" /> <result column="sepc_name" jdbcType="TIMESTAMP" property="sepcName" /> <result column="reply_status" jdbcType="INTEGER" property="replyStatus" /> <result column="reply_content" jdbcType="VARCHAR" property="replyContent" /> <result column="reply_time" jdbcType="TIMESTAMP" property="replyTime" /> <result column="is_show" jdbcType="INTEGER" property="isShow" /> <result column="user_id" jdbcType="VARCHAR" property="userId" /> <result column="username" jdbcType="VARCHAR" property="username" /> <result column="nickname" jdbcType="VARCHAR" property="nickname" /> <result column="user_img" jdbcType="VARCHAR" property="userImg" /> </resultMap> <select id="selectCommontsByProductId" resultMap="ProductCommentsVOMap"> select u.username, u.nickname, u.user_img, c.comm_id, c.product_id, c.product_name, c.order_item_id, c.user_id, c.is_anonymous, c.comm_type, c.comm_level, c.comm_content, c.comm_imgs, c.sepc_name, c.reply_status, c.reply_content, c.reply_time, c.is_show from product_comments c INNER JOIN users u ON u.user_id = c.user_id WHERE c.product_id =#{productId} limit #{start},#{limit} </select>
serevice
添加实体类ProductCommentsVO
@Data @AllArgsConstructor @NoArgsConstructor public class ProductCommentsVO { private String commId; private String productId; private String productName; private String orderItemId; private Integer isAnonymous; private Integer commType; private Integer commLevel; private String commContent; private String commImgs; private Date sepcName; private Integer replyStatus; private String replyContent; private Date replyTime; private Integer isShow; //封装评论对应的用户数据 private String userId; private String username; private String nickname; private String userImg; }
添加分页实体类
@Data @NoArgsConstructor @AllArgsConstructor public class PageHelper<T> { //总记录数 private int count; //总页数 private int pageCount; //分页数据 private List<T> list; }
Service的代码
@Override public ResultVO listCommontsByProductId(String productId,int pageNum,int limit) { //List<ProductCommentsVO> productCommentsVOS = productCommentsMapper.selectCommontsByProductId(productId); //1.根据商品id查询总记录数 Example example = new Example(ProductComments.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("productId",productId); int count = productCommentsMapper.selectCountByExample(example); //2.计算总页数(必须确定每页显示多少条 pageSize = limit) int pageCount = count%limit==0? count/limit : count/limit+1; //3.查询当前页的数据(因为评论中需要用户信息,因此需要连表查询---自定义) int start = (pageNum-1)*limit; List<ProductCommentsVO> list = productCommentsMapper.selectCommontsByProductId(productId, start, limit); return ResultVO.success(list); }
controller
@ApiOperation("商品评价分页查询接口") @GetMapping("/detail-commonts/{pid}") public ResultVO getProductCommonts(@PathVariable("pid") String pid, @RequestParam("pageNum") int pageNum, @RequestParam("limit") int limit){ return productCommontsService.listCommontsByProductId(pid, pageNum, limit); }
接口测试
评价分析
对该商品的评价进行分类,分为好评中评和差评
响应数据如图所示,可以看出是单表查询
service
@Override public ResultVO getCommentsCountByProductId(String productId) { // 查询商品总数 Example example = new Example(ProductComments.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("productId",productId); int total = productCommentsMapper.selectCountByExample(criteria); // 查询好评数 Example example1 = new Example(ProductComments.class); Example.Criteria criteria1 = example1.createCriteria(); criteria1.andEqualTo("productId",productId); criteria1.andEqualTo("commType", 1); int goodTotal = productCommentsMapper.selectCountByExample(example1); // 查询中评数 Example example2 = new Example(ProductComments.class); Example.Criteria criteria2 = example1.createCriteria(); criteria2.andEqualTo("productId",productId); criteria2.andEqualTo("commType", 0); int midTotal = productCommentsMapper.selectCountByExample(example2); // 查询差评数 Example example3 = new Example(ProductComments.class); Example.Criteria criteria3 = example1.createCriteria(); criteria3.andEqualTo("productId",productId); criteria3.andEqualTo("commType", -1); int badTotal = productCommentsMapper.selectCountByExample(example3); // 好评率 double percent = (Double.parseDouble(goodTotal+"")/Double.parseDouble(total + "")) * 100; String percentValue = (percent+"").substring(0,(percent+"").lastIndexOf(".")+3); HashMap<String, Object> hashMap = new HashMap<>(); hashMap.put("total", total); hashMap.put("goodTotal", goodTotal); hashMap.put("badTotal", badTotal); hashMap.put("midTotal", midTotal); hashMap.put("percent", percentValue); return ResultVO.success(hashMap); }
controller
@ApiOperation("商品总体评价信息查询接口") @GetMapping("/detail-commontscount/{pid}") public ResultVO getProductCommontscount(@PathVariable("pid") String pid){ return productCommontsService.getCommentsCountByProductId(pid); }
1.9 购物车业务
添加购物车
Service
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); @Override public ResultVO addShoppingCart(ShoppingCart cart) { cart.setCartTime(sdf.format(new Date())); int i = shoppingCartMapper.insert(cart); if(i>0){ return new ResultVO(ResStatus.OK,"success",null); }else{ return new ResultVO(ResStatus.NO,"fail",null); }
Controller
@PostMapping("/add") public ResultVO addShoppingCart(@RequestBody ShoppingCart cart){ ResultVO resultVO = shoppingCartService.addShoppingCart(cart); return resultVO; }
获取购物车列表
sql
select c.cart_id, c.product_id, c.sku_id, c.user_id, c.cart_num, c.cart_time, c.product_price, c.sku_props,p.product_name, i.url,s.original_price,s.sell_price,s.sku_name from shopping_cart c inner JOIN product p INNER JOIN product_sku s INNER JOIN product_img i ON c.product_id = p.product_id AND c.sku_id=s.sku_id AND c.product_id=i.item_id WHERE c.user_id = 1 AND i.is_main=1
dao
创建一个新的实体类用于保存返回的数据
@Data @NoArgsConstructor @AllArgsConstructor public class ShoppingCartVO { private Integer cartId; private String productId; private String skuId; private String userId; private String cartNum; private String cartTime; private BigDecimal productPrice; private String skuProps; private String productName; private String productImg; private double originalPrice; private double sellPrice; private String skuName; private int skuStock; //库存 }
mapper
public List<ShoppingCartVO> selectShopcartByUserId(int userId);
xml
<resultMap id="ShoppingCartVOMap" type="com.qfedu.fmmall.entity.ShoppingCartVO"> <id column="cart_id" jdbcType="INTEGER" property="cartId" /> <result column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="sku_id" jdbcType="VARCHAR" property="skuId" /> <result column="user_id" jdbcType="VARCHAR" property="userId" /> <result column="cart_num" jdbcType="VARCHAR" property="cartNum" /> <result column="cart_time" jdbcType="VARCHAR" property="cartTime" /> <result column="product_price" jdbcType="DECIMAL" property="productPrice" /> <result column="sku_props" jdbcType="VARCHAR" property="skuProps" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="url" jdbcType="VARCHAR" property="productImg" /> <result column="original_price" jdbcType="VARCHAR" property="originalPrice" /> <result column="sell_price" jdbcType="VARCHAR" property="sellPrice" /> <result column="sku_name" jdbcType="VARCHAR" property="skuName" /> </resultMap> <select id="selectShopcartByUserId" resultMap="ShoppingCartVOMap"> select c.cart_id, c.product_id, c.sku_id, c.user_id, c.cart_num, c.cart_time, c.product_price, c.sku_props, p.product_name, i.url,s.original_price,s.sell_price,s.sku_name from shopping_cart c INNER JOIN product p INNER JOIN product_img i INNER JOIN product_sku s ON c.product_id = p.product_id and i.item_id=p.product_id and c.sku_id=s.sku_id where user_id = #{userId} and i.is_main=1 </select>
service
@Transactional(propagation = Propagation.SUPPORTS) public ResultVO listShoppingCartsByUserId(int userId) { List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByUserId(userId); ResultVO resultVO = new ResultVO(ResStatus.OK, "success", list); return resultVO; }
controller
@GetMapping("/list") @ApiImplicitParam(dataType = "int",name = "userId", value = "用户ID",required = true) public ResultVO list(Integer userId,@RequestHeader("token")String token){ ResultVO resultVO = shoppingCartService.listShoppingCartsByUserId(userId); return resultVO; }
更新购物车
更新购物车发送的请求如下,由分析可以看出url的格式如下,并且发送的是put请求
/shopcart/update/{cartId}/{num}
sql
使用update方法对对应的购物车记录进行修改即可
update shopping_cart set cart_num=3 where cart_id=1
dao
xml
<select id="selectShopcartByUserId" resultMap="ShoppingCartVOMap"> select c.cart_id, c.product_id, c.sku_id, c.user_id, c.cart_num, c.cart_time, c.product_price, c.sku_props, p.product_name, i.url,s.original_price,s.sell_price,s.sku_name from shopping_cart c INNER JOIN product p INNER JOIN product_img i INNER JOIN product_sku s ON c.product_id = p.product_id and i.item_id=p.product_id and c.sku_id=s.sku_id where user_id = #{userId} and i.is_main=1 </select> <update id="updateCartnumByCartid"> update shopping_cart set cart_num=#{cartNum} where cart_id=#{cartId} </update>
mapper
public int updateCartnumByCartid(@Param("cartId") int cartId, @Param("cartNum") int cartNum);
service
@Override public ResultVO updateCartNum(int cartId, int cartNum) { int i = shoppingCartMapper.updateCartnumByCartid(cartId, cartNum); if(i>0){ return new ResultVO(ResStatus.OK,"update success",null); }else{ return new ResultVO(ResStatus.NO,"update fail",null); } }
controller
@PutMapping("/update/{cid}/{cnum}") public ResultVO updateNum(@PathVariable("cid") Integer cartId, @PathVariable("cnum") Integer cartNum, @RequestHeader("token") String token){ ResultVO resultVO = shoppingCartService.updateCartNum(cartId, cartNum); return resultVO; }
获取购物车列表信息
当点击结算时,页面会跳转到结算页面,这里依然需要显示订单中的信息,但是这里需要的是批量结算,所以需要批量获取
从前端发起的请求可以看到cids为本次请求的所有购物车订单
响应结果如下
从中可以看到与获取购物车列表使用到的sql语句大致相同
sql
select c.cart_id, c.product_id, c.sku_id, c.user_id, c.cart_num, c.cart_time, c.product_price, c.sku_props, p.product_name, i.url,s.original_price,s.sell_price,s.sku_name,s.stock from shopping_cart c INNER JOIN product p INNER JOIN product_img i INNER JOIN product_sku s ON c.product_id = p.product_id and i.item_id=p.product_id and c.sku_id=s.sku_id where i.is_main=1 and c.cart_id in (6,7)
dao
创建新的实体类保存结果
/** * 新增 productName、productImg */ @Data @NoArgsConstructor @AllArgsConstructor public class ShoppingCartVO { private Integer cartId; private String productId; private String skuId; private String userId; private String cartNum; private String cartTime; private BigDecimal productPrice; private String skuProps; private String productName; private String productImg; private double originalPrice; private double sellPrice; private String skuName; private int skuStock; //库存 }
Mapper中代码
public List<ShoppingCartVO> selectShopcartByCids(List<Integer> cids);
xml
<resultMap id="ShoppingCartVOMap2" type="com.qfedu.fmmall.entity.ShoppingCartVO"> <id column="cart_id" jdbcType="INTEGER" property="cartId" /> <result column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="sku_id" jdbcType="VARCHAR" property="skuId" /> <result column="user_id" jdbcType="VARCHAR" property="userId" /> <result column="cart_num" jdbcType="VARCHAR" property="cartNum" /> <result column="cart_time" jdbcType="VARCHAR" property="cartTime" /> <result column="product_price" jdbcType="DECIMAL" property="productPrice" /> <result column="sku_props" jdbcType="VARCHAR" property="skuProps" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="url" jdbcType="VARCHAR" property="productImg" /> <result column="original_price" jdbcType="VARCHAR" property="originalPrice" /> <result column="sell_price" jdbcType="VARCHAR" property="sellPrice" /> <result column="sku_name" jdbcType="VARCHAR" property="skuName" /> <result column="stock" property="skuStock" /> </resultMap> <select id="selectShopcartByCids" resultMap="ShoppingCartVOMap2"> select c.cart_id, c.product_id, c.sku_id, c.user_id, c.cart_num, c.cart_time, c.product_price, c.sku_props, p.product_name, i.url,s.original_price,s.sell_price,s.sku_name,s.stock from shopping_cart c INNER JOIN product p INNER JOIN product_img i INNER JOIN product_sku s ON c.product_id = p.product_id and i.item_id=p.product_id and c.sku_id=s.sku_id where i.is_main=1 and c.cart_id in <foreach collection="cids" item="cid" separator="," open="(" close=")"> #{cid} </foreach> </select>
service
@Override public ResultVO listShoppingCartsByCids(String cids) { // 将前端请求携带的字符串转换成int数组 List<Integer> cartIds = new ArrayList<>(); String[] strings = cids.split(","); for (String string : strings) { cartIds.add(Integer.parseInt(string)); } List<ShoppingCartVO> list = shoppingCartMapper.selectShopcartByCids(cartIds); return ResultVO.success(list); }
controller
@GetMapping("/listbycids") @ApiImplicitParam(dataType = "String",name = "cids", value = "选择的购物车记录的id",required = true) public ResultVO listByCids(String cids, @RequestHeader("token")String token){ ResultVO resultVO = shoppingCartService.listShoppingCartsByCids(cids); return resultVO; }
删除购物车
sql
删除数据时,可以批量删除也可以是单个删除,因此使用下面的sql语句
delete from shopping_cart where cart_id IN (10,11)
dao
public int deleteShopcartByCids(List<Integer> cids);
<delete id="deleteShopcartByCids"> delete from shopping_cart where cart_id in <foreach collection="cids" item="cid" separator="," open="(" close=")"> #{cid} </foreach> </delete>
service
@Override public ResultVO deleteShoppingCartsByCids(String cids) { // 将前端请求携带的字符串转换成int数组 List<Integer> cartIds = new ArrayList<>(); String[] strings = cids.split(","); for (String string : strings) { cartIds.add(Integer.parseInt(string)); } int i = shoppingCartMapper.deleteShopcartByCids(cartIds); if(i > 0){ return ResultVO.success(); }else { return ResultVO.failed("删除失败!"); } }
controller
@GetMapping("/delete") @ApiImplicitParam(dataType = "String",name = "cids", value = "选择的购物车记录的id",required = true) public ResultVO deletebycids(String cids, @RequestHeader("token")String token){ ResultVO resultVO = shoppingCartService.deleteShoppingCartsByCids(cids); return resultVO; }
前端代码
由于该功能没有再前端实现因此在shopcart.html页面中加入如下js代码
deleteByIds: function () { if (this.opts.length == 0) { alert("请选择要购买的商品!") } else { //1.获取选择购物车记录的id [0,2] ---> cartId 8 cartId 10 --- 8,10, var cids = ""; for (var i = 0; i < this.opts.length; i++) { var index = this.opts[i]; var cartId = this.shopcarts[index].cartId; if (i < this.opts.length - 1) { cids = cids + cartId + ","; } else { cids = cids + cartId; } } var url = baseUrl + "shopcart/delete/?cids=" + cids; axios({ url: url, method: "get", headers: { token: this.token } }).then((res) => { console.log(res); if (res.data.code = 10000) { //重新刷新购物车 this.getShoppingCartList(); } }); } }
接口测试
1.10 收货地址
获取收获地址
直接根据用户id即可获取到收获地址,所以这里直接使用tkMapper生成的即可。
service
@Transactional(propagation = Propagation.SUPPORTS) public ResultVO listAddrsByUid(int userId) { Example example = new Example(UserAddr.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("userId",userId); criteria.andEqualTo("status",1); List<UserAddr> userAddrs = userAddrMapper.selectByExample(example); ResultVO resultVO = new ResultVO(ResStatus.OK, "success", userAddrs); return resultVO; }
controller
@GetMapping("/list") @ApiImplicitParam(dataType = "int",name = "userId", value = "用户ID",required = true) public ResultVO list(Integer userId,@RequestHeader("token")String token){ ResultVO resultVO = userAddrService.listAddrsByUid(userId); return resultVO; }
1.11 订单业务
订单添加接口
数据库操作
- 根据收获地址的id,获取地址信息
- 根据购物车id,查询购物车的详细信息(关联商品名称,sku,库存,商品图片,价格),用于生成订单快照
- 保存订单
- 保存商品快照
- 修改库存
service
/** * 保存订单业务 * @param cids * @param order * @return * @throws SQLException */ @Transactional public ResultVO addOrder(String cids, Orders order) throws SQLException { //处理cids String[] strings = cids.split(","); List<Integer> cidsInt = new ArrayList<>(); for (String string : strings) { cidsInt.add(Integer.parseInt(string)); } // 查询与当前订单相关联的购物车记录 List<ShoppingCartVO> shopcartList = shoppingCartMapper.selectShopcartByCids(cidsInt); // 判断商品库存是否充足 boolean flag = true; String untitle = ""; // 保存所有商品的名称 最后保存到订单快照中 for (ShoppingCartVO cartVO : shopcartList) { if(Integer.parseInt(cartVO.getCartNum()) > cartVO.getSkuStock()){ flag = false; } untitle = untitle + cartVO.getProductName() + ","; } if(flag){ // 库存充足 则保存订单 // userId // untitle // 支付时间 // 收货人的信息 // 总价格 // 支付方式(1) // 支付状态(待支付) order.setUntitled(untitle); order.setCancelTime(new Date()); order.setStatus("1"); String orderId = UUID.randomUUID().toString().replace("-", ""); order.setOrderId(orderId); int i = ordersMapper.insert(order); // 生成商品快照 for (ShoppingCartVO sc: shopcartList) { int cnum = Integer.parseInt(sc.getCartNum()); String itemId = System.currentTimeMillis()+""+ (new Random().nextInt(89999)+10000); OrderItem orderItem = new OrderItem(itemId, orderId, sc.getProductId(), sc.getProductName(), sc.getProductImg(), sc.getSkuId(), sc.getSkuName(), new BigDecimal(sc.getSellPrice()), cnum, new BigDecimal(sc.getSellPrice() * cnum), new Date(), new Date(), 0); orderItemMapper.insert(orderItem); //增加商品销量 } // 扣减库存 // 使用当前库存减去商品数量 for (ShoppingCartVO cartVO : shopcartList) { String skuId = cartVO.getSkuId(); int newStock = cartVO.getSkuStock() - Integer.parseInt(cartVO.getCartNum()); Example example = new Example(ProductSku.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("skuId", skuId); // ProductSku productSku = productSkuMapper.selectByPrimaryKey(skuId); // productSku.setStock(newStock); // int k = productSkuMapper.updateByExample(productSku, example); ProductSku productSku = new ProductSku(); productSku.setStock(newStock); productSku.setSkuId(skuId); int k = productSkuMapper.updateByPrimaryKeySelective(productSku); } //购买完成后 删除对应的购物车数据 for (Integer cid : cidsInt) { shoppingCartMapper.deleteByPrimaryKey(cid); } return ResultVO.success("生成订单成功!", orderId); }else { return ResultVO.failed("商品库存不足,请重新选择!"); } }
controller
@PostMapping("/add") // @ApiImplicitParam(dataType = "String",name = "cids", value = "购物车id",required = true) public ResultVO list(String cids, @RequestBody Orders order){ System.out.println("##################"); System.out.println(cids); if(cids.contains("#")){ cids.replace("#", ""); } System.out.println(cids); try { Map<String, String> orderInfo = orderService.addOrder(cids, order); String orderId = orderInfo.get("orderId"); if(orderId !=null){ Map<String , String> data = new HashMap<>(); data.put("body",orderInfo.get("productNames")); //商品描述 data.put("out_trade_no",orderId); //使⽤当前⽤户订单的编号作为当前⽀付交易的交易号 data.put("fee_type","CNY"); //⽀付币种 data.put("total_fee", order.getActualAmount()*100 + "" ); //⽀付⾦额 data.put("trade_type","NATIVE"); //交易类型 data.put("notify_url","/pay/success"); //设置⽀付完成时的回调⽅法 WXPay wxPay = new WXPay(new MyPayConfig()); Map<String, String> resp = wxPay.unifiedOrder(data); //发送请求 System.out.println(resp); String code_url = resp.get("code_url"); orderInfo.put("code_url", code_url); return ResultVO.success(orderInfo); }else { return ResultVO.failed("订单为空!"); } } catch (SQLException throwables) { return ResultVO.failed("添加订单失败!"); } catch (Exception e) { e.printStackTrace(); return ResultVO.failed("添加订单失败!"); } }
支付回调
当用户支付成功后,支付平台会向服务器的指定接口发送支付订单的支付状态数据
创建控制器并定义回调接口
package com.qfedu.fmmall.controller; import com.github.wxpay.sdk.WXPayUtil; import com.qfedu.fmmall.entity.Orders; import com.qfedu.fmmall.service.OrderService; import com.qfedu.fmmall.vo.ResultVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.sql.SQLException; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/pay") @Api(value = "用户地址接口",tags = "用户地址管理") @CrossOrigin public class PayController { @Autowired private OrderService orderService; @GetMapping("/callable") public String success(HttpServletRequest request) throws Exception { ServletInputStream is = request.getInputStream(); byte[] bytes = new byte[1024]; int len = -1; StringBuilder builder = new StringBuilder(); while((len = is.read(bytes)) != -1){ builder.append(new String(bytes, 0 ,len)); } String s = builder.toString(); // 使用wxpay的工具类讲xml的响应结果 转换成map Map<String, String> map = WXPayUtil.xmlToMap(s); if(map != null && "success".equalsIgnoreCase(map.get("result_code"))){ // 支付成功 // 修改订单状态为代发货/已支付 String orderId = map.get("out_trade_no"); int i = orderService.updateOrderStatus(orderId, "2"); if(i>0){ // 响应微信平台 HashMap<String, String> resp = new HashMap<>(); resp.put("return_code", "success"); resp.put("return_msg", "OK"); resp.put("appid", map.get("appid")); resp.put("result_code", "success"); String s1 = WXPayUtil.mapToXml(resp); return s1; } } return null; } }
设置回调的url
** 这里涉及到内网穿透的知识点,但是如果部署到云服务器上的可以忽略这一步
订单状态查询
该接口实现比较简单
service
@Override public ResultVO getOrderById(String orderId) { Orders orders = ordersMapper.selectByPrimaryKey(orderId); return ResultVO.success(orders.getStatus()); }
controller
@GetMapping("/status/{oid}") public ResultVO getStatus(@PathVariable("oid") String oid, @RequestHeader("token") String token){ return orderService.getOrderById(oid); }
测试接口
websocket消息推送
创建webSocket服务器
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
添加websocket服务节点配置(Java配置⽅式)
package com.qfedu.fmmall.websocket; 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 getServerEndpointExporter(){ return new ServerEndpointExporter(); } }
创建websocket服务器
package com.qfedu.fmmall.websocket; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; @Component @ServerEndpoint("/webSocket/{oid}") public class WebSocketServer { private static ConcurrentHashMap<String,Session> sessionMap = new ConcurrentHashMap<>(); /**前端发送请求建立websocket连接,就会执行@OnOpen方法**/ @OnOpen public void open(@PathParam("oid") String orderId, Session session){ System.out.println("------------建立连接:"+orderId); sessionMap.put(orderId,session); } /**前端关闭页面或者主动关闭websocket连接,都会执行close**/ @OnClose public void close(@PathParam("oid") String orderId){ sessionMap.remove(orderId); } public static void sendMsg(String orderId,String msg){ try { Session session = sessionMap.get(orderId); session.getBasicRemote().sendText(msg); }catch (Exception e){ e.printStackTrace(); } } }
在PayController中添加使用websocket与前端通信的代码
订单超时取消-定时任务
当用户提交订单后,没有在规定时间内进行支付操作,该订单讲自动取消,还原商品库存信息
- 定时任务(quartz)
- 延时队列(MQ)
首先查询已失效的未支付订单 订单的有效支付时间是半小时,因此判断订单是否失效,就是从当前时间下,向前推半个小时, 如果订单的创建时间不在此范围内则属于失效的订单 需要注意的是,在数据库查询到是 未支付 状态的不一定就一定是未支付,因为支付平台支付成功对服务器响应过程中可能 出现意想不到的问题,所以在修改订单状态之前一定要向支付平台确认此订单的状态,若已经支付则修改支付状态为已支付(2) 若仍然是未支付则取消订单,取消订单后需要修改订单状态 为支付失败(6),并向支付平台通知取消支付链接,恢复商品的库存,就是ProductSku.stock + OrderItem.buy_conuts 这里需要考虑到数据库的并发问题,需要加锁和事务管理,隔离级别是 串行化,以保证数据的正确性和一致性
dao
@Transactional(isolation = Isolation.SERIALIZABLE) // 隔离级别是 串行化 public void closeOrder(String orderId) { synchronized (this){ Orders orders = ordersMapper.selectByPrimaryKey(orderId); orders.setStatus("6"); orders.setCloseType(1); // 失败原因未支付 // 将订单状态改为支付失败 ordersMapper.updateByPrimaryKeySelective(orders); // 根据订单id查询商品快照 Example example = new Example(OrderItem.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("orderId", orderId); List<OrderItem> orderItems = orderItemMapper.selectByExample(example); for (OrderItem orderItem : orderItems) { String skuId = orderItem.getSkuId(); ProductSku productSku = productSkuMapper.selectByPrimaryKey(skuId); productSku.setStock(productSku.getStock() + orderItem.getBuyCounts()); productSkuMapper.updateByPrimaryKeySelective(productSku); } } }
service
@Scheduled(cron = "0/60 * * * * ?") public void checkAndCloseOrder() { // 首先查询已失效的未支付订单 // 订单的有效支付时间是半小时,因此判断订单是否失效,就是从当前时间下,向前推半个小时, // 如果订单的创建时间不在此范围内则属于失效的订单 // 需要注意的是,在数据库查询到是 未支付 状态的不一定就一定是未支付,因为支付平台支付成功对服务器响应过程中可能 // 出现意想不到的问题,所以在修改订单状态之前一定要向支付平台确认此订单的状态,若仍然是 未支付 则取消订单 // 取消订单后需要修改订单状态 为支付失败(6),并向支付平台通知取消支付链接 // 取消订单后,需要恢复商品的库存,就是ProductSku中的stock + OrderItem(订单快照)中的buy_conuts // 这里需要考虑到数据库的并发问题,需要加锁和事务管理 // 1.查询超过三十分钟未支付订单 try{ System.out.println("1——————————————————————----1"); Example example = new Example(Orders.class); Example.Criteria criteria = example.createCriteria(); Date time = new Date(System.currentTimeMillis() - 30 * 60 * 1000); criteria.andLessThan("createTime", time); List<Orders> orders = ordersMapper.selectByExample(example); //2.访问微信平台接口,确认当前订单最终的支付状态 for (int i = 0; i < orders.size(); i++) { Orders order = orders.get(i); HashMap<String, String> params = new HashMap<>(); params.put("out_trade_no", order.getOrderId()); // 使用微信支付提供发接口查询订单的支付状态 Map<String, String> resp = wxPay.orderQuery(params); if("SUCCESS".equalsIgnoreCase(resp.get("trade_state"))){ //2.1 如果订单已经支付,则修改订单状态为"代发货/已支付" status = 2 Orders updateOrder = new Orders(); updateOrder.setOrderId(order.getOrderId()); updateOrder.setStatus("2"); ordersMapper.updateByPrimaryKeySelective(updateOrder); }else if("NOTPAY".equalsIgnoreCase(resp.get("trade_state"))){ //2.2 如果确实未支付 则取消订单: // a.向微信支付平台发送请求,关闭当前订单的支付链接 Map<String, String> map = wxPay.closeOrder(params); System.out.println(map); // b.关闭订单 orderService.closeOrder(order.getOrderId()); } } }catch (Exception e){ e.printStackTrace(); } }
1.12 根据品牌筛选商品
获取某个商品的所有品牌
sql
首先根据分类的category_id获取该分类下的所有商品,再从这些商品中获取到品牌名称
select product_id from product where category_id= cid select select DISTINCT brand from product_params where product_id in cids // 合并 select select DISTINCT brand from product_params where product_id in ( select product_id from product where category_id= cid )
dao
/** * 根据类别id查询此类别下的商品的品牌列表 * @param cid * @return */ public List<String> selectBrandByCategoryId(int cid);
<select id="selectBrandByCategoryId" resultSets="java.util.List" resultType="String"> select DISTINCT brand from product_params where product_id in ( select product_id from product where category_id=#{cid} ) </select>
service
@Override public ResultVO listBrands(int categoryId) { List<String> brands = productMapper.selectBrandByCategoryId(categoryId); return new ResultVO(ResStatus.OK,"success",brands); }
controller
@GetMapping("/listbrands/{cid}") public ResultVO getListBrands(@PathVariable("cid") String cid){ return productService.listBrands(Integer.parseInt(cid)); }
根据分类和品牌分页查询商品
请求url | 响应结果 |
---|---|
product/listbycid/49?pageNum=1&limit=4 |
sql
由响应数据可以看出是分页查询,并且将商品的价格最低的sku一起返回
select * from product_sku where prodoct_id = 1 ORDER BY sell_price limit 0,1 select * from product where category_id = 49
dao
* 根据三级分类ID分页查询商品信息 * @param cid 三级分类id * @param start 起始索引 * @param limit 查询记录数 * @return */ public List<ProductVO> selectProductByCategoryId(@Param("cid") int cid, @Param("start") int start, @Param("limit") int limit);
<resultMap id="ProductVOMap2" type="com.qfedu.fmmall.entity.ProductVO"> <id column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="category_id" jdbcType="INTEGER" property="categoryId" /> <result column="root_category_id" jdbcType="INTEGER" property="rootCategoryId" /> <result column="sold_num" jdbcType="INTEGER" property="soldNum" /> <result column="product_status" jdbcType="INTEGER" property="productStatus" /> <result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> <result column="content" jdbcType="LONGVARCHAR" property="content" /> <!-- 根据商品ID查询价格最低的套餐 --> <collection property="skus" column="product_id" select="com.qfedu.fmmall.dao.ProductSkuMapper.selectLowerestPriceByProductId"/> </resultMap> <select id="selectProductByCategoryId" resultMap="ProductVOMap2"> select product_id, product_name, category_id, root_category_id, sold_num, product_status, content, create_time, update_time from product where category_id=#{cid} limit #{start},#{limit} </select>
selectLowerestPriceByProductId
<resultMap id="BaseResultMap" type="com.qfedu.fmmall.entity.ProductSku"> <id column="sku_id" jdbcType="VARCHAR" property="skuId" /> <result column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="sku_name" jdbcType="VARCHAR" property="skuName" /> <result column="sku_img" jdbcType="VARCHAR" property="skuImg" /> <result column="untitled" jdbcType="VARCHAR" property="untitled" /> <result column="original_price" jdbcType="INTEGER" property="originalPrice" /> <result column="sell_price" jdbcType="INTEGER" property="sellPrice" /> <result column="discounts" jdbcType="DECIMAL" property="discounts" /> <result column="stock" jdbcType="INTEGER" property="stock" /> <result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> <result column="status" jdbcType="INTEGER" property="status" /> </resultMap> <select id="selectLowerestPriceByProductId" resultMap="BaseResultMap"> select sku_id,product_id,sku_name, sku_img,untitled,original_price,sell_price, discounts,stock,create_time,update_time,status from product_sku where product_id = #{productId} ORDER BY sell_price limit 0,1 </select>
service
@Override public ResultVO getProductsByCategoryId(int categoryId, int pageNum, int limit) { //1.查询分页数据 int start = (pageNum-1)*limit; List<ProductVO> productVOS = productMapper.selectProductByCategoryId(categoryId, start, limit); //2.查询当前类别下的商品的总记录数 Example example = new Example(Product.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("categoryId",categoryId); int count = productMapper.selectCountByExample(example); //3.计算总页数 int pageCount = count%limit==0? count/limit : count/limit+1; //4.封装返回数据 PageHelper<ProductVO> pageHelper = new PageHelper<>(count, pageCount, productVOS); return new ResultVO(ResStatus.OK,"SUCCESS",pageHelper); }
controller
@ApiOperation("商品品牌分页查询接口") @GetMapping("/listbycid/{cid}") public ResultVO getProductsByCategoryId(@PathVariable("cid") String cid, @RequestParam("pageNum") int pageNum, @RequestParam("limit") int limit){ return productService.getProductsByCategoryId(Integer.parseInt(cid), pageNum, limit); }
1.13 关键字搜索商品
搜索相关商品的品牌
dao
<select id="selectBrandByKeyword" resultSets="java.util.List" resultType="String"> select DISTINCT brand from product_params where product_id in ( select product_id from product where product_name like #{kw} ) </select>
service
@Override public ResultVO listBrands(String kw) { kw = "%"+kw+"%"; List<String> brands = productMapper.selectBrandByKeyword(kw); return new ResultVO(ResStatus.OK,"SUCCESS",brands); }
搜索出包含关键词的商品
dao
<select id="selectProductByKeyword" resultMap="ProductVOMap2"> select product_id, product_name, category_id, root_category_id, sold_num, product_status, content, create_time, update_time from product where product_name like #{kw} limit #{start},#{limit} </select>
service
@Override public ResultVO searchProduct(String kw, int pageNum, int limit) { //1.查询搜索结果 kw = "%"+kw+"%"; int start = (pageNum-1)*limit; List<ProductVO> productVOS = productMapper.selectProductByKeyword(kw, start, limit); //2.查询总记录数 Example example = new Example(Product.class); Example.Criteria criteria = example.createCriteria(); criteria.andLike("productName",kw); int count = productMapper.selectCountByExample(example); //3.计算总页数 int pageCount = count%limit==0? count/limit:count/limit+1; //4.封装,返回数据 PageHelper<ProductVO> pageHelper = new PageHelper<>(count, pageCount, productVOS); ResultVO resultVO = new ResultVO(ResStatus.OK, "SUCCESS", pageHelper); return resultVO; }
1.14 个人中心
显示所有订单
order/list?userId=15&pageNum=1&limit=5&status=1
dao
OrdersMapper
public List<OrdersVO> selectOrders(@Param("userId") String userId, @Param("status") String status, @Param("start") int start, @Param("limit") int limit);
<resultMap id="OrdersVOMap" type="com.qfedu.fmmall.entity.OrdersVO"> <id column="order_id" jdbcType="VARCHAR" property="orderId" /> <result column="user_id" jdbcType="VARCHAR" property="userId" /> <result column="untitled" jdbcType="VARCHAR" property="untitled" /> <result column="receiver_name" jdbcType="VARCHAR" property="receiverName" /> <result column="receiver_mobile" jdbcType="VARCHAR" property="receiverMobile" /> <result column="receiver_address" jdbcType="VARCHAR" property="receiverAddress" /> <result column="total_amount" jdbcType="DECIMAL" property="totalAmount" /> <result column="actual_amount" jdbcType="INTEGER" property="actualAmount" /> <result column="pay_type" jdbcType="INTEGER" property="payType" /> <result column="order_remark" jdbcType="VARCHAR" property="orderRemark" /> <result column="status" jdbcType="VARCHAR" property="status" /> <result column="delivery_type" jdbcType="VARCHAR" property="deliveryType" /> <result column="delivery_flow_id" jdbcType="VARCHAR" property="deliveryFlowId" /> <result column="order_freight" jdbcType="DECIMAL" property="orderFreight" /> <result column="delete_status" jdbcType="INTEGER" property="deleteStatus" /> <result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> <result column="pay_time" jdbcType="TIMESTAMP" property="payTime" /> <result column="delivery_time" jdbcType="TIMESTAMP" property="deliveryTime" /> <result column="flish_time" jdbcType="TIMESTAMP" property="flishTime" /> <result column="cancel_time" jdbcType="TIMESTAMP" property="cancelTime" /> <result column="close_type" jdbcType="INTEGER" property="closeType" /> <collection property="orderItems" column="order_id" select="com.qfedu.fmmall.dao.OrderItemMapper.listOrderItemsByOrderId"/> </resultMap> <select id="selectOrders" resultMap="OrdersVOMap"> select order_id,user_id,untitled,receiver_name, receiver_mobile,receiver_address, total_amount, actual_amount,pay_type,order_remark,status,delivery_type, delivery_flow_id,order_freight,delete_status,create_time, update_time,pay_time,delivery_time, flish_time,cancel_time,close_type from orders where user_id=#{userId} <if test="status != null"> and status=#{status} </if> limit #{start},#{limit} </select>
OrderItemMapper
public List<OrderItem> listOrderItemsByOrderId(String orderId);
<resultMap id="BaseResultMap" type="com.qfedu.fmmall.entity.OrderItem"> <id column="item_id" jdbcType="VARCHAR" property="itemId" /> <result column="order_id" jdbcType="VARCHAR" property="orderId" /> <result column="product_id" jdbcType="VARCHAR" property="productId" /> <result column="product_name" jdbcType="VARCHAR" property="productName" /> <result column="product_img" jdbcType="VARCHAR" property="productImg" /> <result column="sku_id" jdbcType="VARCHAR" property="skuId" /> <result column="sku_name" jdbcType="VARCHAR" property="skuName" /> <result column="product_price" jdbcType="DECIMAL" property="productPrice" /> <result column="buy_counts" jdbcType="INTEGER" property="buyCounts" /> <result column="total_amount" jdbcType="DECIMAL" property="totalAmount" /> <result column="basket_date" jdbcType="TIMESTAMP" property="basketDate" /> <result column="buy_time" jdbcType="TIMESTAMP" property="buyTime" /> <result column="is_comment" jdbcType="INTEGER" property="isComment" /> </resultMap> <select id="listOrderItemsByOrderId" resultMap="BaseResultMap"> select item_id, order_id, product_id, product_name, product_img, sku_id, sku_name, product_price, buy_counts, total_amount, basket_date, buy_time, is_comment from order_item where order_id=#{orderId} </select>
service
@Override public ResultVO listOrders(String userId, String status, int pageNum, int limit) { Example example = new Example(Orders.class); Example.Criteria criteria = example.createCriteria(); criteria.andLike("userId", userId); if(status != null && "".equals(status)){ criteria.andEqualTo("status",status); } int count = ordersMapper.selectCountByExample(example); //2.计算总页数(必须确定每页显示多少条 pageSize = limit) int pageCount = count%limit==0? count/limit : count/limit+1; int start = (pageNum-1)*limit; List<OrdersVO> ordersVOS = ordersMapper.selectOrders(userId, status, start, limit); PageHelper<OrdersVO> ordersVOPageHelper = new PageHelper<>(count, pageCount, ordersVOS); return ResultVO.success(ordersVOPageHelper); }
controller
@GetMapping("/list") public ResultVO getList(String userId, String status, int pageNum, int limit){ return orderService.listOrders(userId, status, pageNum,limit); }
收货地址管理
2 JWT实现权限认证
2.1 JWT实现
导入JWT依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成对应的token并返回给前端
// 验证成功则生成对应的token
// 使用jwt生成token
JwtBuilder builder = Jwts.builder();
Map<String, Object> map = new HashMap<>();
map.put("key1", "value2");
map.put("key2", "value2");
JwtBuilder jwtBuilder = builder.setSubject(username) //设置subject
.setIssuedAt(new Date()) // 设置token生成的时间
.setId(user.getUserId() + "") // 设置userid为token的唯一id
.setClaims(map) // map中可以存放用户的角色和权限信息
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000 * 2)) // 设置token的过期时间 为两天
.signWith(SignatureAlgorithm.HS256, "sunzy123456");// 设置token的加密方式和加密密钥
String token = jwtBuilder.compact(); // 获取token
return new ResultVO(ResStatus.OK, token, user);
前端进行登录验证时获取对应的token
JWT进行token解析
if(token == null || "".equals(token)){
return new ResultVO(ResStatus.NO, "failed", null);
}else {
JwtParser parser = Jwts.parser();
parser.setSigningKey("sunzy123456"); // 密钥需要与加密时使用的一致
try{
// 如果token正确 且在有效期内 则解析正常否则会出现异常
Jws<Claims> claimsJws = parser.parseClaimsJws(token);
Claims body = claimsJws.getBody(); // 获取token中的用户数据
String subject = body.getSubject(); // 获取token中发subject
String key1 = body.get("key1", String.class); /// 获取添加在map中的值
}catch(UnsupportedJwtException e){
return new ResultVO(ResStatus.NO, "token不合法请重新登录!", null);
}catch(ExpiredJwtException e){
return new ResultVO(ResStatus.NO, "token已过期,请重新登录!", null);
}
catch (Exception e){
return new ResultVO(ResStatus.NO, "未知错误", null);
}
使用拦截器验证token
创建拦截器
package com.sunzy.fmmall.interceptor; import com.alibaba.fastjson.JSON; import com.sunzy.fmmall.vo.ResStatus; import com.sunzy.fmmall.vo.ResultVO; import io.jsonwebtoken.*; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class CheckTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getParameter("token"); if (token == null || "".equals(token)) { doResponse(response, "请先登录!"); return false; } else { JwtParser parser = Jwts.parser(); parser.setSigningKey("sunzy123456"); // 密钥需要与加密时使用的一致 try { // 如果token正确 且在有效期内 则解析正常否则会出现异常 Jws<Claims> claimsJws = parser.parseClaimsJws(token); Claims body = claimsJws.getBody(); // 获取token中的用户数据 String subject = body.getSubject(); // 获取token中发subject String key1 = body.get("key1", String.class); /// 获取添加在map中的值 return true; } catch (UnsupportedJwtException e) { doResponse(response,"token不合法,请重新登录!"); } catch (ExpiredJwtException e) { doResponse(response,"token已过期,请重新登录!"); } catch (Exception e) { doResponse(response,"未知错误!"); } return false; } } private void doResponse(HttpServletResponse response, String msg) throws IOException { ResultVO resultVO = new ResultVO(ResStatus.NO, msg, null); String string = JSON.toJSONString(resultVO); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); PrintWriter writer = response.getWriter(); writer.write(string); writer.flush(); writer.close(); } }
配置拦截器
package com.sunzy.fmmall.config; import com.sunzy.fmmall.interceptor.CheckTokenInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 拦截器的配置类 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Autowired private CheckTokenInterceptor checkTokenInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // registry是拦截器的注册器 // 将自己创建的拦截器加入进来 即可实现拦截功能 registry.addInterceptor(checkTokenInterceptor) .addPathPatterns("/**") // 拦截所有路径 .excludePathPatterns("/user/**"); // 除了用户登录和注册路径 } }
2.2 通过header传递token
前端访问受限资源时,都必须携带token发送请求,token可以放在请求行(params)、请求头(header)以及请求体(data),但是一般默认放在请求头中
前端使用axios
axios({ url:url, method:"get", headers:{ token:token }, params:{ userId:userId } })
浏览器的预检机制
只要是带自定义header的跨域请求,在发送真实请求前都会先发送OPTIONS请求,浏览器根据OPTIONS请求返回的结果来决定是否继续发送真实的请求进行跨域资源访问。所以复杂请求肯定会两次请求服务端。
因此需要在拦截器中加入以下代码,必要响应第一次的OPTIONS请求后,才能收到第二次的GET请求。
String method = request.getMethod(); System.out.println(method); if("OPTIONS".equals(method)){ return true; }
3.通用实体类
3.1 用于响应的实体类
package com.qfedu.fmmall.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.annotation.Resource;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "ResultVO对象",description = "封装接口返回给前端的数据")
public class ResultVO {
//响应给前端的状态码
@ApiModelProperty(value = "响应状态码",dataType = "int")
private int code;
//响应给前端的提示信息
@ApiModelProperty("响应提示信息")
private String msg;
//响应给前端的数据
@ApiModelProperty("响应数据")
private Object data;
public static ResultVO success(){
return new ResultVO(ResStatus.OK, "success", null);
}
public static ResultVO success(String msg){
return new ResultVO(ResStatus.OK, msg, null);
}
public static ResultVO success(Object obj){
return new ResultVO(ResStatus.OK, "success", obj);
}
public static ResultVO failed(){
return new ResultVO(ResStatus.NO, "failed", null);
}
public static ResultVO failed(String msg){
return new ResultVO(ResStatus.NO, msg, null);
}
}
4 微信支付
通过微信平台为商家提供代收款服务
4.1 商户注册微信支付业务
- 商户编号:1497984412
- 商户账号AppID:wx632c8f211f8122c6
- 商户Key:sbNCm1JnevqI36LrEaxFwcaT0hkGxFnC
4.2 商户向微信申请支付订单,即支付短链接
导入微信支付的依赖
<dependency> <groupId>com.github.wxpay</groupId> <artifactId>wxpay-sdk</artifactId> <version>0.0.3</version> </dependency>
创建WXPayConfig,重写里面的方法,设置自己的AppID,商户id,密钥,
public class MyPayConfig implements WXPayConfig { @Override public String getAppID() { return "wx632c8f211f8122c6"; } @Override public String getMchID() { return "1497984412"; } @Override public String getKey() { return "sbNCm1JnevqI36LrEaxFwcaT0hkGxFnC"; } @Override public InputStream getCertStream() { return null; } @Override public int getHttpConnectTimeoutMs() { return 0; } @Override public int getHttpReadTimeoutMs() { return 0; } }
设置⽀付订单的参数
HashMap<String,String> data = new HashMap<>(); data.put("body","咪咪萧条"); //商品描述 data.put("out_trade_no",orderId); //使⽤当前⽤户订单的编号作为当前⽀付交易的 交易号 data.put("fee_type","CNY"); //⽀付币种 data.put("total_fee","1"); //⽀付⾦额 data.put("trade_type","NATIVE"); //交易类型 data.put("notify_url","/pay/success"); //设置⽀付完成时的回调⽅法 接⼝
申请支付连接
WXPay wxPay = new WXPay(new MyPayConfig()); Map<String, String> resp = wxPay.unifiedOrder(data); //发送请求 orderInfo.put("payUrl",resp.get("code_url"));
5 Ngrok实现内网穿透
注册Ngrok后进行实名认证
开通隧道
获取隧道ID
下载ngork客户端
启动客户端,输入对应的隧道ID,即可实现内网穿透
6 quartz定时任务框架使⽤
6.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
6.2 创建定时任务
@Component
public class PrintHelloWorldJob {
//https://cron.qqe2.com
@Scheduled(cron = "0/3 * * * * ?")
public void printHelloWorld(){
System.out.println("----hello world.");
}
}
6.3 在启动类添加注解以开启定时任务
@SpringBootApplication
@EnableScheduling
public class QuartzDemoApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzDemoApplication.class, args);
}
}
当控制台打印出这段日志时,说明定时任务创建成功
7 后端项目部署
使用docker部署项目
Dockerfile
FROM java:8-alpine COPY ./app.jar /tmp/app.jar EXPOSE 8080 ENTRYPOINT java -jar /tmp/app.jar
项目打包
将jar包放到与dockerfile同一目录下
上传到服务器中构建镜像
进入到Dockerfile所在的目录下,运行以下命令
docker build -t fmmall .
创建容器
docker run --name fmmall -p 8080:8080 -d fmmall
分布式
使用watch dog机制监视redis中key的过期时间
看门狗线程:用于给当前的key延长过期时间,保证业务线程正常执行过程,锁不会过期
分布式锁框架-redisson
基于redis+看门狗实现的分布式锁框架
ES
springboot整合ES
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
配置信息
spring: elasticsearch: rest: uris: http://192.168.238.132:9200
将数据库中的信息导入到ES中
如果商品表中没有数据,则在平台管理系统中的商品添加功能中,当商家向商品表添加并上架一个商品时同步向ES添加一个商品;商家下架一个商品就从ES中删除一个商品。
系统运行前期数据量小没有使用ES,当数据量增长之后使用ES时,需要将数据库现有的数据导入到ES(导入工作需要在项目部署到生产环境之前来完成)// 1.查询数据库获取到所有数据 List<ProductVO> productVOS = productMapper.selectProducts(); int size = productVOS.size(); // return ResultVO.success(size); // 2. 遍历数据将数据写入到ES中 this.client =new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.238.132:9200") )); for (int i = 0; i < productVOS.size(); i++) { ProductVO productVO = productVOS.get(i); String productId = productVO.getProductId(); String productName = productVO.getProductName(); Integer soldNum = productVO.getSoldNum(); List<ProductSku> skus = productVO.getSkus(); String skuImg = skus.size() == 0? "": productVO.getSkus().get(0).getSkuImg(); String skuName = skus.size() == 0? "": productVO.getSkus().get(0).getSkuName(); Integer sellPrice = skus.size() == 0? 0: productVO.getSkus().get(0).getSellPrice(); Product4ES product4ES = new Product4ES(productId, productName, skuImg, soldNum, skuName, sellPrice); IndexRequest request = new IndexRequest("fmmallproductindex"); request.id(productId).source(JSON.toJSONString(product4ES), XContentType.JSON); IndexResponse index = this.client.index(request, RequestOptions.DEFAULT); System.out.println("------ i " + i + "-----" + index); } this.client.close();
使用ES进行进行全文搜索
/*从ES中查询信息*/ this.client =new RestHighLevelClient(RestClient.builder( HttpHost.create("http://192.168.238.132:9200") )); int start = (pageNum-1)*limit; SearchRequest request = new SearchRequest("fmmallproductindex"); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.multiMatchQuery(kw, "productName", "pruductSkuName")); // 分页条件 sourceBuilder.from(start); sourceBuilder.size(limit); // 高亮显示 HighlightBuilder highlightBuilder = new HighlightBuilder(); HighlightBuilder.Field productName = new HighlightBuilder.Field("productName"); HighlightBuilder.Field pruductSkuName = new HighlightBuilder.Field("pruductSkuName"); highlightBuilder.field(productName); highlightBuilder.field(pruductSkuName); highlightBuilder.preTags("<label style='color:red'>"); highlightBuilder.postTags("<label>"); sourceBuilder.highlighter(highlightBuilder); request.source(sourceBuilder); SearchResponse searchResponse = null; try { searchResponse = this.client.search(request, RequestOptions.DEFAULT); } catch (IOException e) { e.printStackTrace(); } // 处理响应结果 SearchHits hits = searchResponse.getHits(); TotalHits totalHits = hits.getTotalHits(); int count = (int) totalHits.value; // 查询到的记录总数 int pageCount = (count%limit==0? count/limit:count/limit+1); Iterator<SearchHit> iterator = hits.iterator(); List<Product4ES> product4ESList = new ArrayList<>(); while(iterator.hasNext()){ SearchHit nextHit = iterator.next(); String sourceAsString = nextHit.getSourceAsString(); Product4ES product4ES = JSON.parseObject(sourceAsString, Product4ES.class); // 获取高亮字段 Map<String, HighlightField> highlightFields = nextHit.getHighlightFields(); HighlightField highLightProductName = highlightFields.get("productName"); if(highLightProductName != null){ String string = Arrays.toString(highLightProductName.fragments()); product4ES.setProductName(string); } product4ESList.add(product4ES); } PageHelper<Product4ES> pageHelper = new PageHelper<>(count, pageCount, product4ESList); ResultVO resultVO = new ResultVO(ResStatus.OK, "SUCCESS", pageHelper); return resultVO;
搭建服务发现和注册中心Eureka
添加关于erueka相关配置信息
eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka register-with-eureka: false fetch-registry: false server: port: 8761 spring: application: name: eureka-server
在项目的启动类中添加注释
@EnableEurekaServer
package com.qfedu.eureka.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
配置security信息
spring: application: name: eureka-server security: user: name: sunzy password: 111111
配置springsecuity
``` ### 拆分用户登录模块 - 首先创建新的模块 api-user-login - 导入依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
配置application.yml
server: port: 8001 eureka: client: service-url: defualtZone: http://sunzy:111111@127.0.0.1:8761/eureka spring: application: name: api-user-login
在启动类添加注解
@EnableDiscoveryClient
和@EnableFeignClients
package com.qfedu.api; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class ApiUserLoginApplication { public static void main(String[] args) { SpringApplication.run(ApiUserLoginApplication.class, args); } }
添加注解后既可启动服务,在注册中心中可以看到注册成功的服务
用户查询模块
该模块的主要的作用是为登录提供用户查询功能,因此涉及到服务调用
首先创建模块user-check
pom文件内容
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qfedu</groupId> <artifactId>user-check</artifactId> <version>2.1.1</version> <name>user-check</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.3</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>com.qfedu</groupId> <artifactId>common</artifactId> <version>2.0.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yaml
server: port: 9001 spring: application: name: user-check datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://81.68.252.36:3306/fmmall?characterEncoding=utf-8&useSSL=false username: root password: root mybatis-plus: global-config: db-config: # table-prefix: tbl_ id-type: auto configuration: # 设置mybatisplus的日志为标准输入格式 可以显示执行的sql语句、携带的参数与查询结果 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl eureka: client: service-url: defaultZone: http://sunzy:111111@127.0.0.1:8761/eureka
在启动类中添加注释
@EnableDiscoveryClient
service和dao层代码与之前写内容基本一致
在api-user-login模块中调用user-check
创建UserCheckClient
@FeignClient(value = "user-check") public interface UserCheckClient { @GetMapping("user/check") public Users userCheck(@RequestParam("username") String username,@RequestParam("password") String password); }
其中
@FeignClient(value = "user-check")
中的value要与注册到eureka中服务名相同,@GetMapping("user/check")
要与user-check的controller中的访问路径相同。方法名也要与controller中的方法相同。并且在有多个参数的情况下需要在每个参数前加上
@RequestParam("username")
,否则fegin调用时,会将所有的参数当成post方式的请求体,从而报错。public Users userCheck(@RequestParam("username") String username,@RequestParam("password") String password);
api-user-login在service中进行服务调用即可
package com.qfedu.api.service.impl; import com.qfedu.api.service.UserService; import com.qfedu.api.service.fegin.UserCheckClient; import com.qfedu.fmmall.beans.Users; import com.qfedu.fmmall.vo.ResultVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Qualifier("com.qfedu.api.service.fegin.UserCheckClient") @Autowired private UserCheckClient userCheckClient; @Override public ResultVO checkLogin(String username, String password) { // 调用另外一个服务从数据库中查询到用户信息 Users users = userCheckClient.userCheck(username, password); if(users == null) { return ResultVO.failed("密码错误!"); }else if ("null".equals(users.getNickname())){ return ResultVO.failed("用户名不存在!"); }else { return ResultVO.success(users); } } }
搭建gateway
创建gateway服务模块
pom文件内容
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.qfedu</groupId> <artifactId>gateway</artifactId> <version>2.1.1</version> <name>gateway</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>2021.0.3</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
在application.yml中配置相关信息
server: port: 8000 spring: application: name: gateway cloud: gateway: routes: - id: api-service uri: http://127.0.0.1:8001 predicates: - Path=/user/login - id: api-service uri: http://127.0.0.1:8002 predicates: - Path=/user/regist
实现全局过滤器
当用户访问8000端口时,所有的请求都会被该过滤器拦截
因此全局过滤器可以进行权限验证
package com.qfedu.gateway.filter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.List; @Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("-------------------"); System.out.println("进入全局过滤器"); List<String> token = exchange.getRequest().getHeaders().get("token"); if(token != null && token.size() > 0){ // 对token进行验证,如果验证通过则放行本次拦截 return chain.filter(exchange); }else { // 验证不通过,则返回对应的状态码 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 拦截请求 return exchange.getResponse().setComplete(); } } @Override public int getOrder() { return 0; } }
动态路由配置
使用服务注册与发现实现动态路由
将gateway服务模块加入到eureka中,即添加依赖添加配置文件
eureka: client: service-url: defaultZone: http://sunzy:111111@127.0.0.1:8761/eureka
使用注册到eureka中的服务名替换ip地址
lb代表使用负载均衡模式,默认策略时轮询
server: port: 8000 spring: application: name: gateway main: web-application-type: reactive cloud: gateway: routes: - id: api-login uri: lb://api-user-login #uri: http://127.0.0.1:8001 predicates: - Path=/user/login - id: api-regist uri: lb://api-user-regist predicates: - Path=/user/regist eureka: client: service-url: defaultZone: http://sunzy:111111@127.0.0.1:8761/eureka
Gateway限流
基于令牌桶实现的网关限流,使用redis作为桶结合过滤器实现限流
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.9.0</version> </dependency>
配置application.yml
server: port: 8000 spring: application: name: gateway main: web-application-type: reactive cloud: gateway: routes: - id: api-login uri: http://127.0.0.1:8001 # uri: lb://api-user-login predicates: - Path=/user/login #限流策略配置 filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 #令牌桶每s的填充速度 redis-rate-limiter.burstCapacity: 2 # 令牌桶容量 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@keyResolver}" - id: api-regist uri: lb://api-user-regist predicates: - Path=/user/regist redis: host: 127.0.0.1 port: 6379 database: 0 password: # lettuce: # pool: # max-active: 10 # max-wait: 1000 # max-idle: 5 # min-idle: 3 eureka: client: service-url: defaultZone: http://sunzy:111111@127.0.0.1:8761/eureka
配置keyResolver
package com.qfedu.gateway.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; @Configuration public class AppConfig { @Bean public KeyResolver keyResolver() { //http://localhost:9999/order/query?user=1 //使⽤请求中的user参数的值作为令牌桶的key //return exchange ->Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); return exchange -> Mono.just(exchange.getRequest().getURI().getPath()); } }
当用户访问的速度超过了令牌的产生速度时就会无法访问,返回429的状态码
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!