fmmall

本文最后更新于:9 个月前

商城项目

项目地址:https://github.com/sunzhengyu99/fmmall/tree/master

体验地址:http://www.fmmall.top/

1.业务逻辑开发

1.1登录注册

1.1.1 完成dao操作

  1. 创建实体类

    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;
    }
    
  2. 编写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);
    }
    
  3. 编写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功能

  1. 创建接口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);
    
    }
  2. 创建实现类 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 接口测试

image-20220710210231769

image-20220710210251564

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注解即可允许后端响应数据进行跨域响应。

image-20220712100855420

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中,用于前端传值

image-20220712103807945

在index.html的created函数中添加以下代码

var token = getCookieValue("token");
if(token !=null && token !=""){
    this.isLogin = true;
    this.username = getCookieValue("username");
    this.userimg = getCookieValue("userImg");
}

获取到用户的基本信息,再通过v-model显示到前端页面中。

image-20220712103740058

1.4 首页轮播图

  • 数据库操作实现

    image-20220712104939741

  • 编写sql语句

    image-20220712105512286

    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 分类列表

接口开发

  • 数据库分析

    image-20220712130956027

  • 添加实体类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());
        }

    实测方法二比方法一的响应速度要快很多倍,所以还是减少在数据库中进行数据的处理。

    方法一 方法二
    image-20220712173631776 image-20220712173546986

    实现效果如下

    image-20220712174710162

1.6 商品推荐

推荐商品原则可以是 1.根据商城销量推荐2.推荐商城最新上架的商品

说明:商品推荐算法是根据多个维度进行权重计算的结果,计算出一个匹配值

  • 数据库操作

    image-20220712175743195

    image-20220712175819883

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

    测试结果

    image-20220712183632458

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

    接口测试

    image-20220712235308867

    前端实现效果

    image-20220712235328090

1.8 商品详情

点击首页推荐的商品、轮播图商品广告、商品列表页面点击商品,就会进入到商品的详情页面

用户点击时,携带商品的id进行后端请求,后端接收到商品id后,进行数据库查询,返回详细信息

包括以下内容

1.商品的基本信息

2.商品的套餐信息

3.商品的图片信息

4.商品的评价信息

5.商品的参数信息

image-20220713104403656

商品详情接口

image-20220713110111555

接口所需信息如下,只需要三个单表查询即可完成,因此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);
          }
      
      }

    接口测试

    image-20220713110939926

商品参数接口

image-20220713111914400

接口所需数据可知,也为单表查询,因此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);
    }

    测试结果

    image-20220713112142962

商品评价接口

评价接口需要完成两个功能:评价列表分页展示和评价分析

  • 评价列表分页展示

    image-20220713113518231

    接口所需参数如上图,可以看出需要关联用户数据,因此为多表关联查询

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

      接口测试

      image-20220713150611632

  • 评价分析

    对该商品的评价进行分类,分为好评中评和差评

    image-20220713112838049

    image-20220713113028811

    响应数据如图所示,可以看出是单表查询

    • 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 购物车业务

添加购物车

image-20220713151431751

  • 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}

image-20220714105311322

  • 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为本次请求的所有购物车订单

image-20220714124912790

响应结果如下

image-20220714125042189

从中可以看到与获取购物车列表使用到的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)

    image-20220714125332136

  • 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;
    }
    

    image-20220714130445019

删除购物车

  • 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();
                }
            });
        }
    }

    image-20220714134933507

    接口测试

    image-20220714135115137

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 订单业务

image-20220714172108540

订单添加接口

  • 数据库操作

    • 根据收获地址的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

    image-20220715163058828

** 这里涉及到内网穿透的知识点,但是如果部署到云服务器上的可以忽略这一步

订单状态查询

该接口实现比较简单

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

    测试接口

    image-20220715173458440

websocket消息推送

image-20220715174726117

创建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与前端通信的代码

    image-20220715223718929

订单超时取消-定时任务

当用户提交订单后,没有在规定时间内进行支付操作,该订单讲自动取消,还原商品库存信息

  • 定时任务(quartz)
  • 延时队列(MQ)
image-20220715224137251
首先查询已失效的未支付订单
订单的有效支付时间是半小时,因此判断订单是否失效,就是从当前时间下,向前推半个小时,
如果订单的创建时间不在此范围内则属于失效的订单
需要注意的是,在数据库查询到是 未支付 状态的不一定就一定是未支付,因为支付平台支付成功对服务器响应过程中可能
出现意想不到的问题,所以在修改订单状态之前一定要向支付平台确认此订单的状态,若已经支付则修改支付状态为已支付(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));
    }

    image-20220716105333765

根据分类和品牌分页查询商品

请求url 响应结果
product/listbycid/49?pageNum=1&limit=4 image-20220716105625379
  • 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);
    }

    image-20220716114341669

1.13 关键字搜索商品

image-20220716114519584

搜索相关商品的品牌

  • 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

image-20220711172838166

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后进行实名认证

  • 开通隧道

    image-20220715164520718

  • 获取隧道ID

    image-20220715164621244

  • 下载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);
 }
}

当控制台打印出这段日志时,说明定时任务创建成功

image-20220716085923884

7 后端项目部署

使用docker部署项目

  • Dockerfile

    FROM java:8-alpine
    COPY ./app.jar /tmp/app.jar
    EXPOSE 8080
    ENTRYPOINT java -jar /tmp/app.jar
  • 项目打包

    image-20220719123949027

  • 将jar包放到与dockerfile同一目录下

    image-20220719124037998

  • 上传到服务器中构建镜像

    进入到Dockerfile所在的目录下,运行以下命令

    docker build -t fmmall .

    image-20220719124317986

  • 创建容器

    docker run --name fmmall -p 8080:8080 -d  fmmall

    image-20220719124407926

分布式

使用watch dog机制监视redis中key的过期时间

image-20220726222800912

看门狗线程:用于给当前的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;
    

image-20220730122320909

搭建服务发现和注册中心Eureka

image-20220730180822713
  • 添加关于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);
        }
    
    }

    添加注解后既可启动服务,在注册中心中可以看到注册成功的服务

    image-20220730224027994

用户查询模块

该模块的主要的作用是为登录提供用户查询功能,因此涉及到服务调用

  • 首先创建模块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的状态码

image-20220731190403002


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

 目录

Copyright © 2020 my blog
载入天数... 载入时分秒...