乐优商城(三)

目录

后台功能——商品分类管理

一、数据

二、页面实现

2.1 页面分析

2.2 功能实现

2.2.1 url异步请求

2.2.2 后台接口实现

2.3 解决跨域请求

2.3.1 什么是跨域

2.3.2 解决跨域问题的方案

2.3.3 cors解决跨域问题

2.3.4 重新测试

2.4 功能完善

2.4.1 异步查询工具axios

2.4.2 QS工具

2.4.3 增加分类

2.4.4 删除分类

2.4.5 修改分类

三、效果展示

四、遇到的问题

4.1 分类增加的时候发生的问题

4.2 解决方法

4.2.1 前端代码修改

4.2.2 后端代码修改


后台功能——商品分类管理

 商城中最重要的就是商品,当商品数目增多后,需要对商品进行分类,而且不同的商品会有不同的品牌信息。具体关系如下图所示:

  • 一个商品分类下有很多商品

  • 一个商品分类下有很多品牌

  • 而一个品牌,可能属于不同的分类

  • 一个品牌下也会有很多商品

一、数据

点击下载

导入后:

二、页面实现

效果图:

2.1 页面分析

采用树结构展示,页面对应的是/pages/item/Category.vue

<template>
  <v-card>
      <v-flex xs12 sm10>
        <v-tree ref="tree"  url="/item/category/list"
                :isEdit="isEdit"
                @handleAdd="handleAdd"
                @handleEdit="handleEdit"
                @handleDelete="handleDelete"
                @handleClick="handleClick"
        />
      </v-flex>
  </v-card>
</template>

这里面最主要的就是自定义组件v-tree,具体的使用方法如下:

属性列表

属性名称 说明 数据类型 默认值
url 用来加载数据的地址,即延迟加载 String -
isEdit 是否开启树的编辑功能 boolean false
treeData 整颗树数据,这样就不用远程加载了 Array -

这里推荐使用url进行延迟加载,每当点击父节点时,就会发起请求,根据父节点id查询子节点信息。当有treeData属性时,就不会触发url加载。

远程请求返回的结果格式:

事件列表

事件名称 说明 回调参数
handleAdd 新增节点时触发,isEdit为true时有效 新增节点node对象,包含属性:name、parentId和sort
handleEdit 当某个节点被编辑后触发,isEdit为true时有效 被编辑节点的id和name
handleDelete 当删除节点时触发,isEdit为true时有效 被删除节点的id
handleClick 点击某节点时触发 被点击节点的node对象,包含全部信息

一个node的完整信息

  • 父节点

  • 子节点

2.2 功能实现

2.2.1 url异步请求

  • 先按下图进行修改

  • 然后刷新页面,打开调试可以看到发送的请求地址如下图所示

  • 最后需要做的就是编写后台接口

2.2.2 后台接口实现

实体类

在leyou-item-interface中添加category实体类:


@Table(name="tb_category")
/**
 * @author li
 * @time 2018/8/7
 * @feature: 商品分类对应的实体
 */
public class Category implements Serializable {
	@Id
	@GeneratedValue(strategy= GenerationType.IDENTITY)
	private Long id;
	private String name;
	private Long parentId;
	private Boolean isParent;
	/**
	 * 排序指数,越小越靠前
	 */
	private Integer sort;

     //get和set方法省略
     //注意isParent的set和get方法
}

需要注意的是,这里要用到jpa的注解,因此我们在ly-item-iterface中添加jpa依赖

    <dependencies>
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>persistence-api</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

Controller

编写一个controller一般需要知道四个内容:

  • 请求方式:决定我们用GetMapping还是PostMapping

  • 请求路径:决定映射路径

  • 请求参数:决定方法的参数

  • 返回值结果:决定方法的返回值

在刚才页面发起的请求中,我们就能得到绝大多数信息:

  • 请求方式:Get

  • 请求路径:/api/item/category/list。其中/api是网关前缀,/item是网关的路由映射,真实的路径应该是/category/list

  • 请求参数:pid=0,根据tree组件的说明,应该是父节点的id,第一次查询为0,那就是查询一级类目

  • 返回结果:json数组

/**
 * @Author: 98050
 * Time: 2018-08-07 19:18
 * Feature:
 */
@RestController
@RequestMapping("category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 根据父节点查询商品类目
     * @param pid
     * @return
     */
    @GetMapping("/list")
    public ResponseEntity<List<Category>> queryCategoryByPid(@RequestParam("pid") Long pid){

        List<Category> list=this.categoryService.queryCategoryByPid(pid);
        if (list == null){
            //没有找到返回404
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
        //找到返回200
        return ResponseEntity.ok(list);
    }
}

Servicer

定义对应的接口和实现类


/**
 * @Author: 98050
 * Time: 2018-08-07 19:16
 * Feature: 分类的业务层
 */
public interface CategoryService {

    /**
     * 根据id查询分类
     * @param pid
     * @return
     */
    List<Category> queryCategoryByPid(Long pid);
}
/**
 * @Author: 98050
 * Time: 2018-08-07 19:16
 * Feature: 分类的业务层
 */
@Service
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    

    /**
     * 根据父节点id查询分类
     * @param pid
     * @return
     */
    @Override
    public List<Category> queryCategoryByPid(Long pid) {
        Category t=new Category();
        t.setParentId(pid);
        return this.categoryMapper.select(t);
    }
}

Mapper

使用通用mapper来简化开发:

注意:一定要加上注解@org.apache.ibatis.annotations.Mapper

/**
 * @Author: 98050
 * @Time: 2018-08-07 19:15
 * @Feature:
 */
@org.apache.ibatis.annotations.Mapper
public interface CategoryMapper extends Mapper<Category> {
}

测试

不经过网关,直接访问:

通过网关访问:

后台页面访问:

报错,跨域问题~

2.3 解决跨域请求

2.3.1 什么是跨域

跨域是指跨域名的访问,以下情况都属于跨域:

跨域原因说明 示例
域名不同 www.jd.comwww.taobao.com
域名相同,端口不同 www.jd.com:8080www.jd.com:8081
二级域名不同 item.jd.commiaosha.jd.com

如果域名和端口都相同,但是请求路径不同,不属于跨域,如:

www.jd.com/item

www.jd.com/goods

但刚才是从manage.leyou.com去访问api.leyou.com,这属于二级域名不同,所以会产生跨域。 

跨域不一定会有跨域问题。因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是于当前页同域名的路径,这能有效的阻止跨站攻击。因此:跨域问题 是针对ajax的一种限制。

2.3.2 解决跨域问题的方案

目前比较常用的跨域解决方案有3种:

  • Jsonp

    最早的解决方案,利用script标签可以跨域的原理实现。

    限制:

    • 需要服务的支持

    • 只能发起GET请求

  • nginx反向代理

    思路是:利用nginx反向代理把跨域为不跨域,支持各种请求方式

    缺点:需要在nginx进行额外配置,语义不清晰

  • CORS

    规范化的跨域请求解决方案,安全可靠。

    优势:

    • 在服务端进行控制是否允许跨域,可自定义规则

    • 支持各种请求方式

    缺点:

    • 会产生额外的请求

2.3.3 cors解决跨域问题

  • 什么是cors?

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

  • 浏览器端:

    目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。

  • 服务端:

    CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否运行其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。

  • 原理剖析

浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。

简单请求

只要同时满足以下两大条件,就属于简单请求。:

(1) 请求方法是以下三种方法之一:

  • HEAD

  • GET

  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept

  • Accept-Language

  • Content-Language

  • Last-Event-ID

  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

当浏览器发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.

 Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

如果服务器允许跨域,需要在返回的响应头中携带下面信息:

Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意

  • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

注意:

如果跨域请求要想操作cookie,需要满足3个条件:

  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。

  • 浏览器发起ajax需要指定withCredentials 为true

  • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

特殊请求

 不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。

预检请求

特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

一个“预检”请求的样板:

OPTIONS /cors HTTP/1.1
Origin: http://manage.leyou.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

与简单请求相比,除了Origin以外,多了两个头:

  • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT

  • Access-Control-Request-Headers:会额外用到的头信息

预检请求的响应

服务的收到预检请求,如果许可跨域,会发出响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

除了Access-Control-Allow-OriginAccess-Control-Allow-Credentials以外,这里又额外多出3个头:

  • Access-Control-Allow-Methods:允许访问的方式

  • Access-Control-Allow-Headers:允许携带的头

  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

  • 实现

虽然原理比较复杂,但是:

  • 浏览器端都有浏览器自动完成,我们无需操心

  • 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写。

事实上,SpringMVC已经帮我们写好了CORS的跨域过滤器:CorsFilter ,内部已经实现了刚才所讲的判定逻辑,我们直接用就好了。

ly-api-gateway中编写一个配置类,并且注册CorsFilter:

package com.leyou.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @author li
 * @time:2018/8/7
 * 处理跨域请求的过滤器
 */
@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        //1.添加CORS配置信息
        CorsConfiguration config = new CorsConfiguration();

        //1) 允许的域,不要写*,否则cookie就无法使用了
        config.addAllowedOrigin("http://manage.leyou.com");
        //2) 是否发送Cookie信息
        config.setAllowCredentials(true);
        //3) 允许的请求方式
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        // 4)允许的头信息
        config.addAllowedHeader("*");

        //2.添加映射路径,我们拦截一切请求
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);

        //3.返回新的CorsFilter.
        return new CorsFilter(configSource);
    }
}

2.3.4 重新测试

页面:

2.4 功能完善

目前只完成了显示功能,需要添加增、删、改功能

2.4.1 异步查询工具axios

异步查询数据,自然是通过ajax查询,但jQuery与MVVM的思想不吻合,而且ajax只是jQuery的一小部分。因此不可能为了发起ajax请求而去引用这么大的一个库,所以使用Vue官方推荐的ajax请求框架:axios。

入门

axios的GET请求语法:

axios.get("/item/category/list?pid=0") // 请求路径和请求参数拼接
    .then(function(resp){
    	// 成功回调函数
	})
    .catch(function(){
    	// 失败回调函数
	})
// 参数较多时,可以通过params来传递参数
axios.get("/item/category/list", {
        params:{
            pid:0
        }
	})
    .then(function(resp){})// 成功时的回调
    .catch(function(error){})// 失败时的回调

axios的POST请求语法: (新增一个用户)

axios.post("/user",{
    	name:"Jack",
    	age:21
	})
    .then(function(resp){})
    .catch(function(error){})

注意:POST请求传参,不需要像GET请求那样定义一个对象,在对象的params参数中传参。post()方法的第二个参数对象,就是将来要传递的参数 。

配置

而在项目中,已经引入了axios,并且进行了简单的封装,在src下的http.js中:

import Vue from 'vue'
import axios from 'axios'
import config from './config'
// config中定义的基础路径是:http://api.leyou.com/api
axios.defaults.baseURL = config.api; // 设置axios的基础请求路径
axios.defaults.timeout = 3000; // 设置axios的请求时间

Vue.prototype.$http = axios;// 将axios赋值给Vue原型的$http属性,这样所有vue实例都可使用该对象

通过简单封装后,以后使用this.$http就可以发起相应的请求了。

2.4.2 QS工具

QS是一个第三方库,即Query String,请求参数字符串 。

什么是请求参数字符串?例如: name=jack&age=21

QS工具可以便捷的实现 JS的Object与QueryString的转换。

通过this.$qs获取这个工具。

为什么要使用qs?

因为axios处理请求体的原则会根据请求数据的格式来定:

  • 如果请求体是对象:会转为json发送

  • 如果请求体是String:会作为普通表单请求发送,但需要我们自己保证String的格式是键值对。

    如:name=jack&age=12

所以在使用axios传递参数时,需要把json转换成拼接好的字符串。

2.4.3 增加分类

2.4.3.1 实现逻辑

首先将新添加的节点信息存入数据库,然后修改其父节点的相应信息isParent = true 

2.4.3.2 前端

在/pages/item/Category.vue中完善handleAdd方法,传入的参数是node,即当前节点的详细信息,使用qs进行转换,然后发送请求。

handleAdd(node) {
  this.$http({
    method:'post',
    url:'/item/category',
    data:this.$qs.stringify(node)
  }).then().catch();
}

2.4.3.3 后台接口

Controller

  • 请求方式:新增使用PostMapping

  • 请求路径:/api/item/category。其中/api是网关前缀,/item是网关的路由映射,真实的路径应该是/category

  • 请求参数:节点参数(node)

  • 返回值结果:如果添加成功就返回201 CREATED

    /**
     * 保存
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> saveCategory(Category category){
        System.out.println(category);
        this.categoryService.saveCategory(category);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

Service

接口

    /**
     * 保存
     * @param category
     */
    void saveCategory(Category category);

实现类

    @Override
    public void saveCategory(Category category) {
        /**
         * 将本节点插入到数据库中
         * 将此category的父节点的isParent设为true
         */
        //1.首先置id为null
        category.setId(null);
        //2.保存
        this.categoryMapper.insert(category);
        //3.修改父节点
        Category parent = new Category();
        parent.setId(category.getParentId());
        parent.setIsParent(true);
        this.categoryMapper.updateByPrimaryKeySelective(parent);

    }

Mapper

依旧使用通用mapper提供的方法insert和updateByPrimaryKeySelective

2.4.4 删除分类

2.4.4.1 实现逻辑

删除比较麻烦一点,首先要确定被删节点是父节点还是子节点。如果是父节点,那么删除所有附带子节点,然后要维护中间表。如果是子节点,那么只删除自己,然后判断父节点孩子的个数,如果不为0,则不做任何修改;如果为0,则修改父节点isParent的值为false,最后维护中间表。

中间表:tb_category_brand。是用来维护分类和品牌的对应关系的,一个分类下会有多个品牌,某个品牌可能属于不同的分类。所以中间表中保存的数据就是category_id和brand_id,而且category_id中保存的是最底一层的类目id,也就是分类的叶子节点id。

当对中间表进行修改的时候,那么就需要找出当前节点下所有的叶子节点(如果是父节点),然后删除中间表中的对应数据。

2.4.4.2 前端

在/pages/item/Category.vue中完善handleDelete方法,传入参数是要删除的节点id,然后发送请求。

handleDelete(id) {
  console.log("delete ... " + id)
  this.$http.delete("/item/category/cid/"+id).then(() =>{
    this.$message.info("删除成功!");
  }).catch(() =>{
    this.$message.info("删除失败!");
  })
}

2.4.4.3 后端

Controller

  • 请求方式:删除使用DeleteMapping

  • 请求路径:/api/item/category/cid/{cid}。其中/api是网关前缀,/item是网关的路由映射,真实的路径应该是/category/cid/{cid}

  • 请求参数:请求参数为cid,使用@PathVariable("cid")获取

  • 返回值结果:如果添加成功就返回200 OK

    /**
     * 删除
     * @return
     */
    @DeleteMapping("cid/{cid}")
    public ResponseEntity<Void> deleteCategory(@PathVariable("cid") Long id){
        this.categoryService.deleteCategory(id);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

Service

接口

    /**
     * 删除
     * @param id
     */
    void deleteCategory(Long id);

实现类

    @Override
    public void deleteCategory(Long id) {
        Category category=this.categoryMapper.selectByPrimaryKey(id);
        if(category.getIsParent()){
            //1.查找所有叶子节点
            List<Category> list = new ArrayList<>();
            queryAllLeafNode(category,list);

            //2.查找所有子节点
            List<Category> list2 = new ArrayList<>();
            queryAllNode(category,list2);

            //3.删除tb_category中的数据,使用list2
            for (Category c:list2){
                this.categoryMapper.delete(c);
            }

            //4.维护中间表
            for (Category c:list){
                this.categoryMapper.deleteByCategoryIdInCategoryBrand(c.getId());
            }

        }else {
            //1.查询此节点的父亲节点的孩子个数 ===> 查询还有几个兄弟
            Example example = new Example(Category.class);
            example.createCriteria().andEqualTo("parentId",category.getParentId());
            List<Category> list=this.categoryMapper.selectByExample(example);
            if(list.size()!=1){
                //有兄弟,直接删除自己
                this.categoryMapper.deleteByPrimaryKey(category.getId());

                //维护中间表
                this.categoryMapper.deleteByCategoryIdInCategoryBrand(category.getId());
            }
            else {
                //已经没有兄弟了
                this.categoryMapper.deleteByPrimaryKey(category.getId());

                Category parent = new Category();
                parent.setId(category.getParentId());
                parent.setIsParent(false);
                this.categoryMapper.updateByPrimaryKeySelective(parent);
                //维护中间表
                this.categoryMapper.deleteByCategoryIdInCategoryBrand(category.getId());
            }
        }
    }

分析:

  • 先判断此节点是否是父节点
  • 如果是,则:
  1. 需要一个可以查询所有叶子节点的函数queryAllLeafNode,参数有两个,一个是父节点,另一个是用来接收子节点的list。
  2. 需要一个可以查询所有子节点的函数queryAllNode,参数同上。
  3. 删除tb_category中的数据直接使用通用mappper中的方法即可。
  4. 维护中间表tb_category_brand时,需要在mapper中自定义方法deleteByCategoryIdInCategoryBrand,根据category的id删除对应的数据。
  • 如果不是,则:
  1. 查询此节点还有几个兄弟节点
  2. 如果有兄弟,则直接删除自己,维护中间表
  3. 如果没有兄弟,先删除自己,然后修改父节点的isParent为false,最后维护中间表

查询子节点和叶子节点的函数:

    /**
     * 查询本节点下所包含的所有叶子节点,用于维护tb_category_brand中间表
     * @param category
     * @param leafNode
     */
    private void queryAllLeafNode(Category category,List<Category> leafNode){
        if(!category.getIsParent()){
            leafNode.add(category);
        }
        Example example = new Example(Category.class);
        example.createCriteria().andEqualTo("parentId",category.getId());
        List<Category> list=this.categoryMapper.selectByExample(example);

        for (Category category1:list){
            queryAllLeafNode(category1,leafNode);
        }
    }

    /**
     * 查询本节点下所有子节点
     * @param category
     * @param node
     */
    private void queryAllNode(Category category,List<Category> node){

        node.add(category);
        Example example = new Example(Category.class);
        example.createCriteria().andEqualTo("parentId",category.getId());
        List<Category> list=this.categoryMapper.selectByExample(example);

        for (Category category1:list){
            queryAllNode(category1,node);
        }
    }

Mapper

    /**
     * 根据category id删除中间表相关数据
     * @param cid
     */
    @Delete("DELETE FROM tb_category_brand WHERE category_id = #{cid}")
    void deleteByCategoryIdInCategoryBrand(@Param("cid") Long cid);

其他的方法都是通用mapper提供的:selectByPrimaryKey、delete、selectByExample、updateByPrimaryKeySelective、deleteByPrimaryKey

2.4.5 修改分类

2.4.5.1 实现逻辑

因为修改只能修改分类的名字,所以比较简单,直接更新数据库中对应id的name即可。

2.4.5.2 前端

在/pages/item/Category.vue中完善handleEdit方法,传入的参数是id和name,即要修改的节点id及新的name,先构造一个json,然后使用qs进行转换,最后发送请求。

handleEdit(id,name) {
  const node={
    id:id,
    name:name
  }
  this.$http({
    method:'put',
    url:'/item/category',
    data:this.$qs.stringify(node)
  }).then(() => {
    this.$message.info("修改成功!");
  }).catch(() => {
    this.$message.info("修改失败!");
  });
}

需要注意的是项目中提供的tree组件在进行修改时会发生错误,原因是位于/src/components/tree/TreeItem.vue中,afterEdit方法中if条件中的判断出了问题,应该是this.beginEdit,而不是this.model.beginEdit。

afterEdit() {
        if (this.beginEdit) {
          this.beginEdit = false;
          this.handleEdit(this.model.id, this.model.name);
        }
}

2.4.5.3 后端

Controller

  • 请求方式:修改使用PutMapping

  • 请求路径:/api/item/category。其中/api是网关前缀,/item是网关的路由映射,真实的路径应该是/category

  • 请求参数:节点参数(node)

  • 返回值结果:如果添加成功就返回202 ACCEPTED

    /**
     * 更新
     * @return
     */
    @PutMapping
    public ResponseEntity<Void> updateCategory(Category category){
        this.categoryService.updateCategory(category);
        return  ResponseEntity.status(HttpStatus.ACCEPTED).build();
    }

Service

接口

    /**
     * 更新
     * @param category
     */
    void updateCategory(Category category);

实现类

    @Override
    public void updateCategory(Category category) {
        this.categoryMapper.updateByPrimaryKeySelective(category);
    }

Mapper

依旧使用通用mapper中的updateByPrimaryKeySelective方法。

三、效果展示

http://bmob-cdn-15811.b0.upaiyun.com/2018/08/29/174db4e840c2045c804dd908228b8a59.mp4

四、遇到的问题

4.1 分类增加的时候发生的问题

  • 增加的逻辑:构造一个新节点,然后将这个新节点插入到”树“中。那么新节点的数据为:

  • 自定义组件中新增节点代码(/src/components/tree/TreeItem.vue):
      addChild: function () {
        let child = {
          id: 0,
          name: '新的节点',
          parentId: this.model.id,
          isParent: false,
          sort:this.model.children? this.model.children.length + 1:1
        }
        if (!this.model.isParent) {
          Vue.set(this.model, 'children', [child]);
          this.model.isParent = true;
          this.open = true;
          this.handleAdd(child);
        } else {
          if (!this.isFolder) {
            this.$http.get(this.url, {params: {pid: this.model.id}}).then(resp => {
              Vue.set(this.model, 'children', resp.data);
              this.model.children.push(child);
              this.open = true;
              this.handleAdd(child);
            });
          } else {
            this.model.children.push(child);
            this.open = true;
            this.handleAdd(child);
          }
        }
      }
  • 问题场景

假设在图书、音像、电子书刊分类下新增一个分类名为Node1。点击新增按钮,然后修改名字为Node1。此时这个节点已经插入到数据库中,但是这个节点的信息在新增完毕后并没有同步到前端页面当中,所以当前节点的id = 0。那么,以Node1作为父节点,在新增子节点时就会产生问题,因为Node1的id没有同步,一直是0,所以在创建它的子节点时,子节点的parentId就为0了,即发生新增失败。

4.2 解决方法

最好的解决方法就是:当新增完毕后,刷新一下tree组件中的数据,而且必须保证树的展开层次与刷新前一致。但是由于对Vue并不是很熟悉,tree这个组件只是初步掌握,这种方法没有实现。

笨办法:因为数据库中tb_category这个表的id字段是自增的,所以在新增前可以获取数据库中最后一条数据的id,在构造新节点时,给id赋的值就是最后一条数据的id加1。

同时对增加功能进行优化,必须在选中(即必须点击父节点)的情况下才能进行新增。

4.2.1 前端代码修改

先发送请求获取数据库中最后一条记录,然后得到id,构造新节点,插入。

      addChild: function () {
        this.$http.get(this.url,{params: {pid:-1}}).then(resp => {
          let child = {
            id: resp.data[0].id+1,
            name: '新的节点',
            parentId: this.model.id,
            isParent: false,
            sort:this.model.children? this.model.children.length + 1:1
          };
          if (this.isSelected) {
            if (!this.model.isParent) {
              Vue.set(this.model, 'children', [child]);
              this.model.isParent = true;
              this.open = true;
              this.handleAdd(child);
            } else {
              if (!this.isFolder) {
                this.$http.get(this.url, {params: {pid: this.model.id}}).then(resp => {
                  Vue.set(this.model, 'children', resp.data);
                  this.model.children.push(child);
                  this.open = true;
                  this.handleAdd(child);
                });
              } else {
                this.model.children.push(child);
                this.open = true;
                this.handleAdd(child);
              }
            }
          }else {
            this.$message.error("选中后再操作!");
          }
        });
      },

4.2.2 后端代码修改

Controller

修改原来的queryCategoryByPid方法,当传入的id为-1时,查找最后一条数据,否则根据实际id查询。

    @GetMapping("/list")
    public ResponseEntity<List<Category>> queryCategoryByPid(@RequestParam("pid") Long pid){

        //如果pid的值为-1那么需要获取数据库中最后一条数据
        if (pid == -1){
            List<Category> last = this.categoryService.queryLast();
            return ResponseEntity.ok(last);
        }
        else {
            List<Category> list = this.categoryService.queryCategoryByPid(pid);
            if (list == null) {
                //没有找到返回404
                return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
            }
            //找到返回200
            return ResponseEntity.ok(list);
        }
    }

Service

接口

    /**
     * 查询当前数据库中最后一条数据
     * @return
     */
    List<Category> queryLast();

实现类

    /**
     * 查询数据库中最后一条数据
     * @return
     */
    @Override
    public List<Category> queryLast() {
        List<Category> last =this.categoryMapper.selectLast();
        return last;
    }

Mapper

提供查询最后一条数据的方法

    /**
     * 查询最后一条数据
     * @return
     */
    @Select("SELECT * FROM `tb_category` WHERE id = (SELECT MAX(id) FROM tb_category)")
    List<Category> selectLast();

猜你喜欢

转载自blog.csdn.net/lyj2018gyq/article/details/82150316