乐忧商城项目总结-2

6.商品分类

6.1 搭建后台管理的前端页面

在这里插入图片描述
webpack:是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。并且提供了前端项目的热部署插件。
我们最主要理清index.html、main.js、App.vue之间的关系(这部分属于前端,目前能看懂会用就行):
在这里插入图片描述

  • index.html:html模板文件。定义了空的div,其id为app。
  • main.js:实例化vue对象,并且通过id选择器绑定到index.html的div中,因此main.js的内容都将在index.html的div中显示。main.js中使用了App组件,即App.vue,也就是说index.html中最终展现的是App.vue中的内容。index.html引用它之后,就拥有了vue的内容(包括组件、样式等),所以,main.js也是webpack打包的入口。
  • index.js:定义请求路径和组件的映射关系。相当于之前的
  • App.vue中也没有内容,而是定义了vue-router的锚点:,我们之前讲过,vue-router路由后的组件将会在锚点展示。
  • 最终结论:一切路由后的内容都将通过App.vue在index.html中显示。
  • 访问流程:用户在浏览器输入路径,例如:http://localhost:9001/#/item/brand --> index.js(/item/brand路径对应pages/item/Brand.vue组件) --> 该组件显示在App.vue的锚点位置 --> main.js使用了App.vue组件,并把该组件渲染在index.html文件中(id为“app”的div中)
    在这里插入图片描述

6.2 Vuetify框架

Vue虽然会帮我们进行视图的渲染,但样式还是由我们自己来完成。这显然不是我们的强项,因此后端开发人员一般都喜欢使用一些现成的UI组件,拿来即用,常见的例如:

  • BootStrap(基于Jquery,之前写第一个的时候用过)
  • LayUI
  • EasyUI
  • ZUI

然而这些UI组件的基因天生与Vue不合,因为他们更多的是利用DOM操作,借助于jQuery实现,而不是MVVM的思想。

使用Vuetify的原因:

  • Vuetify几乎不需要任何CSS代码,而element-ui许多布局样式需要我们来编写
  • Vuetify从底层构建起来的语义化组件。简单易学,容易记住。
  • Vuetify基于Material Design(谷歌推出的多平台设计规范),更加美观,动画效果酷炫,且风格统一

项目页面布局
在这里插入图片描述

在这里插入图片描述
里面使用了Vuetify中的2个组件和一个布局元素:

  • v-navigation-drawer :导航抽屉,主要用于容纳应用程序中的页面的导航链接。
  • v-toolbar:工具栏通常是网站导航的主要途径。可以与导航抽屉一起很好地工作,动态选择是否打开导航抽屉,实现可伸缩的侧边栏。
  • v-content:并不是一个组件,而是标记页面布局的元素。可以根据您指定的app组件的结构动态调整大小,使得您可以创建高度可定制的组件。

v-content中的内容来自哪里?
在这里插入图片描述

  • Layout映射的路径是/
  • 除了Login以外的所有组件,都是定义在Layout的children属性,并且路径都是/的下面
  • 因此当路由到子组件时,会在Layout中定义的锚点中显示。
  • 并且Layout中的其它部分不会变化,这就实现了布局的共享。

6.3 使用域名访问本地项目

我们现在访问页面使用的是ip访问
实际开发中,会有不同的环境:

  • 开发环境:自己的电脑
  • 测试环境:提供给测试人员使用的环境
  • 预发布环境:数据是和生成环境的数据一致,运行最新的项目代码进去测试
  • 生产环境:项目最终发布上线的环境

如果不同环境使用不同的ip去访问,可能会出现一些问题。为了保证所有环境的一致,我们会在各种环境下都使用域名来访问。
当前后台前端项目leyou-manage-web的访问ip地址为http://localhost:9001,我们希望将其域名改为www.manage.leyou.com

域名解析的原理
一个域名一定会被解析为一个或多个ip。这一般会包含两步:

  • 本地域名解析
    浏览器会首先在本机的hosts文件中查找域名映射的IP地址,如果查找到就返回IP ,没找到则进行域名服务器解析,一般本地解析都会失败,因为默认这个文件是空的。

    • Windows下的hosts文件地址:C:/Windows/System32/drivers/etc/hosts
    • Linux下的hosts文件所在路径: /etc/hosts
  • 域名服务器解析

    • 本地解析失败,才会进行域名服务器解析,域名服务器就是网络中的一台计算机,里面记录了所有注册备案的域名和ip映射关系,一般只要域名是正确的,并且备案通过,一定能找到。

我们不可能去购买一个域名,因此我们可以伪造本地的hosts文件,实现对域名的解析。修改本地的host为:

# SwitchHosts!

# leyou
127.0.0.1 www.manage.leyou.com # 后台页面
127.0.0.1 www.api.leyou.com # zuul网关
192.168.124.121 image.leyou.com # 图片服务器地址,使用虚拟机来模拟图片服务器
127.0.0.1 www.leyou.com # 前台页面

nginx解决端口问题
nginx可以作为web服务器,但更多的时候,我们把它作为网关,因为它具备网关必备的功能:

  • 反向代理

  • 负载均衡

  • 动态路由

  • 请求过滤
    Web服务器分2类:

  • web应用服务器,如:

    • tomcat
    • resin
    • jetty
  • web服务器,如:

    • Apache 服务器
    • Nginx
    • IIS

区分:web服务器不能解析jsp等页面,只能处理js、css、html等静态资源。

并发:web服务器的并发能力远高于web应用服务器

个人总结的nginx的几个特点:1.作为web服务器使用时能够支持50000个并发量,一个tomcat的并发量好像也就200?2.nginx安装很简单,配置文件也简洁,启动容易,而且几乎不会出什么bug,可以一直运行,甚至可以在不间断服务的情况下进行软件升级。3.nginx同时也是一款非常优秀的邮件代理服务器。

nginx的反向代理
什么是反向代理?

  • 代理:通过客户机的配置,实现让一台服务器代理客户机,客户的所有请求都交给代理服务器处理。
  • 反向代理:用一台服务器,代理真实服务器,用户访问时,不再是访问真实服务器,而是代理服务器。

nginx可以当做反向代理服务器来使用:

  • 我们需要提前在nginx中配置好反向代理的规则,不同的请求,交给不同的真实服务器处理
  • 当请求到达nginx,nginx会根据已经定义的规则进行请求的转发,从而实现路由功能

利用反向代理,就可以解决我们前面所说的端口问题,如图
在这里插入图片描述
项目编写完成后我本机的nginx配置文件如下:


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
	
	server {
        listen       80;
        server_name  www.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Server $host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		
		location /api/goods/item {
			# 先找本地
			# root html;
			# if (!-f $request_filename) { #请求的文件不存在,就反向代理
				proxy_pass http://127.0.0.1:10010;
			#	break;
			# }
		}
		
		location /api/goods {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
		
		location / {
			proxy_pass http://127.0.0.1:9002;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
    }

    server {
        listen       80;
        server_name  www.manage.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Server $host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		
		location / {
			proxy_pass http://127.0.0.1:9001;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
    }
	
	server {
        listen       80;
        server_name  image.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Server $host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		
		location / {
			# root D:\\data\\java-program\\IdeaProjects\\leyou\\leyou-upload\\src\\images;
			proxy_pass http://192.168.124.121;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
    }

	server {
        listen       80;
        server_name  www.api.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Server $host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Host $host;
		
		location /api/upload {
			proxy_pass http://127.0.0.1:8082;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
			
			rewrite "^/api/(.*)$" /$1 break;
		}
		
		location / {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
		}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

下图是对配置文件的一些解析:
在这里插入图片描述
nginx中的每个server就是一个反向代理配置,可以有多个server

下图展示了经过域名和nginx配置后访问项目的整个流程:
在这里插入图片描述

  1. 浏览器准备发起请求,访问http://mamage.leyou.com,但需要进行域名解析
  2. 优先进行本地域名解析,因为我们修改了hosts,所以解析成功,得到地址:127.0.0.1
  3. 请求被发往解析得到的ip,并且默认使用80端口:http://127.0.0.1:80
    本机的nginx一直监听80端口,因此捕获这个请求
  4. nginx中配置了反向代理规则,将manage.leyou.com代理到127.0.0.1:9001,因此请求被转发
  5. 后台系统的webpack server监听的端口是9001,得到请求并处理,完成后将响应返回到nginx
  6. nginx将得到的结果返回到浏览器

6.4 实现商品分类查询

下面是category表的一些字段:

CREATE TABLE `tb_category` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目id',
  `name` varchar(20) NOT NULL COMMENT '类目名称',
  `parent_id` bigint(20) NOT NULL COMMENT '父类目id,顶级类目填0',
  `is_parent` tinyint(1) NOT NULL COMMENT '是否为父节点,0为否,1为是',
  `sort` int(4) NOT NULL COMMENT '排序指数,越小越靠前',
  PRIMARY KEY (`id`),
  KEY `key_parent_id` (`parent_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1424 DEFAULT CHARSET=utf8 COMMENT='商品类目表,类目和商品(spu)是一对多关系,类目与品牌是多对多关系';

因为商品分类会有层级关系,因此这里我们加入了parent_id字段,对本表中的其它分类进行自关联。
注意sort字段用来表示同一个分类层级的排序顺序
在这里插入图片描述

当进入分类管理页面时,浏览器会向服务器发送一条请求:http://www.api.leyou.com/api/item/category/list?pid=0
至于为什么会发送这样一条请求,这就和用vuetify写的后台页面有关了,这里就不具体分析了。
如果刚开始什么其它操作都不做,就会出现一个关键问题:跨域问题

跨域问题
跨域:浏览器对于javascript的同源策略的限制 。

以下情况都属于跨域:

跨域原因说明示例
域名不同www.jd.com 与 www.taobao.com
域名相同,端口不同www.jd.com:8080 与 www.jd.com:8081
二级域名不同item.jd.com 与 miaosha.jd.com
  • 如果域名和端口都相同,但是请求路径不同,不属于跨域,如:

www.jd.com/item
www.jd.com/goods

  • http和https也属于跨域

而我们刚才是从manage.leyou.com去访问www.api.leyou.com,这属于二级域名不同,跨域了。

为什么有跨域问题?
跨域不一定都会有跨域问题。

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

因此:跨域问题 是针对ajax的一种限制。

但是这却给我们的开发带来了不便,而且在实际生产环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?
解决跨域问题的方案
目前比较常用的跨域解决方案有3种:

  • Jsonp
    最早的解决方案,利用script标签可以跨域的原理实现。
    限制:
    • 需要服务的支持
    • 只能发起GET请求
  • nginx反向代理
    思路是:利用nginx把跨域反向代理为不跨域,支持各种请求方式
    缺点:需要在nginx进行额外配置,语义不清晰
  • CORS
    规范化的跨域请求解决方案,安全可靠。
    优势:
    • 在服务端进行控制是否允许跨域,可自定义规则
    • 支持各种请求方式
      缺点:
    • 会产生额外的请求

我们这里会采用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-urlencoded、multipart/form-data、text/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-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

  • Access-Control-Allow-Methods:允许访问的方式
  • Access-Control-Allow-Headers:允许携带的头
  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

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

虽然原理比较复杂,但是实现起来比较简单:

  • 浏览器端都有浏览器自动完成,我们无需操心
  • 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写。

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

这里直接粘上我写的代码:

package com.leyou.gateway.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;

@Configuration
public class LeyouCorsConfiguration {
    @Bean
    public CorsFilter corsFilter(){
        // 初始化cors配置对象
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.addAllowedOrigin("http://www.manage.leyou.com"); //允许跨域的域名,如果要携带cookie,不能写*。 *:代表所有域名都可以跨域访问
        configuration.addAllowedOrigin("http://www.leyou.com"); //允许跨域的域名,如果要携带cookie,不能写*。 *:代表所有域名都可以跨域访问
        //configuration.addAllowedOrigin("http://localhost:9001");
        // configuration.addAllowedOrigin("http://www.api.leyou.com");
        configuration.setAllowCredentials(true); //允许携带cookie
        configuration.addAllowedHeader("*"); //跨域时允许携带任何头信息
        configuration.addAllowedMethod("*"); // 代表所有的请求方法:GET,POST,PUT,...

        // 初始化cors配置源对象
        UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);

        // 返回corsFilter实例,参数: cors配置源对象
        CorsFilter corsFilter = new CorsFilter(urlBasedCorsConfigurationSource);
        return corsFilter;
    }
}

项目写完后再来访问时发现了403错误:
在这里插入图片描述
可是我明明已经写了Cors的配置类啊,nginx,zuul,eureka都运行正常,zuul网关配置的过滤器也直接放行了,可为什么还是403错误呢?弄了半天发现原因竟是我没有通过域名来访问:
在这里插入图片描述
可以发现我直接在一个新的页面直接输入访问地址是可以访问的:
在这里插入图片描述

我是直接从http://localhost:9001来访问后台页面的,直接绕过了nginx和zuul网关,这时候明显跨域了,但是我之前自己编写的Cors配置类没有允许http://localhost:9001跨域,所以自然就会出现403错误,想通了这一点,只需要在代码中加上这一行即可:

configuration.addAllowedOrigin("http://localhost:9001");

经过测试没问题了,果然是因为这个原因:
在这里插入图片描述

既然已经解决了跨域问题,那么关于商品分类查询的实现就直接粘上我写的代码吧!在leyou-item-service模块中新建三个包分别表示控制层(Controller),业务层(Service)和持久层(mapper,之前用ssm框架的时候是dao层,由于这里使用了通用框架mapper于是写为mapper层)
Category实体类:

package com.leyou.item.pojo;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Table(name="tb_category")
public class Category {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Long parentId;
    private Boolean isParent; // 注意isParent生成的getter和setter方法需要手动加上Is
    private Integer sort;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public Boolean getIsParent() {
        return isParent;
    }

    public void setIsParent(Boolean parent) {
        isParent = parent;
    }

    public Integer getSort() {
        return sort;
    }

    public void setSort(Integer sort) {
        this.sort = sort;
    }
}

CategoryController类:

package com.leyou.item.controller;

import com.leyou.item.pojo.Category;
import com.leyou.item.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;
import java.util.stream.Collectors;

@Controller
@RequestMapping("category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 根据父亲节点的id查询所有的子节点
     * @param pid
     * @return
     */
    @GetMapping("list")
    public ResponseEntity<List<Category>> queryByPid(@RequestParam(value = "pid",defaultValue = "0") Long pid){
        if(pid == null || pid < 0){
            // 400:参数不合法
            //return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
            //return  new ResponseEntity<>(HttpStatus.BAD_REQUEST);
            return ResponseEntity.badRequest().build();
        }
        List<Category> categories = this.categoryService.queryByPid(pid);
        if(CollectionUtils.isEmpty(categories)){
            // 404: 资源服务器未找到
            //return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
            System.out.println("resource is not found!");
            return ResponseEntity.notFound().build();
        }
        // 200 查询成功
        return ResponseEntity.ok(categories);
        // 500 服务器内部错误, 这个只要出错,会自动报状态码500,因此这里可以不使用try,catch来处理.
    }

    @GetMapping
    public ResponseEntity<List<String>> queryNamesById(@RequestParam("ids")List<Long> ids){
        if(CollectionUtils.isEmpty(ids)){
            return ResponseEntity.badRequest().build();
        }
        List<Category> categories = this.categoryService.queryByIds(ids);
        List<String> strings = categories.stream().map(category -> category.getName()).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(strings)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(strings);
    }
}

CategoryServiceImpl类:

package com.leyou.item.service.impl;

import com.leyou.item.pojo.Category;
import com.leyou.item.mapper.CategoryMapper;
import com.leyou.item.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CategoryServiceImpl implements CategoryService {
    @Autowired
    private CategoryMapper categoryMapper;

    /**
     * 根据父节点的id查询所有的子节点
     * @param pid
     * @return
     */
    @Override
    public List<Category> queryByPid(Long pid) {
        Category category = new Category();
        category.setParentId(pid);
        return categoryMapper.select(category);
    }

    /**
     * 根据id数组查询所有节点
     * @param ids
     * @return
     */
    @Override
    public List<Category> queryByIds(List<Long> ids){
        return this.categoryMapper.selectByIdList(ids);
    }
}

CategoryMapper类:

package com.leyou.item.mapper;

import com.leyou.item.pojo.Category;
import tk.mybatis.mapper.additional.idlist.SelectByIdListMapper;
import tk.mybatis.mapper.common.Mapper;

public interface CategoryMapper extends Mapper<Category>, SelectByIdListMapper<Category,Long> {
}


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

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

还需要注意的是:我们并没有在mapper接口上声明@Mapper注解,那么mybatis如何才能找到接口呢?
我们在启动类上添加一个扫描包功能:

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.leyou.item.mapper") // mapper接口的包扫描
public class LeyouItemServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(LeyouItemServiceApplication.class, args);
    }
}

7.品牌查询

首先是品牌对应的数据库表:

CREATE TABLE `tb_brand` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
  `name` varchar(50) NOT NULL COMMENT '品牌名称',
  `image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',
  `letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';

这里需要注意的是,品牌和商品分类之间是多对多关系。因此我们有一张中间表,来维护两者间关系:

CREATE TABLE `tb_category_brand` (
  `category_id` bigint(20) NOT NULL COMMENT '商品类目id',
  `brand_id` bigint(20) NOT NULL COMMENT '品牌id',
  PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';

但是,这张表中并没有设置外键约束,似乎与数据库的设计范式不符。为什么这么做?

  • 外键会严重影响数据库读写的效率
  • 数据删除时会比较麻烦

在电商行业,性能是非常重要的。宁可在代码中通过逻辑来维护表关系,也不设置外键。

接下来是实体类:

package com.leyou.item.pojo;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Table(name = "tb_brand")
public class Brand {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;// 品牌名称
    private String image;// 品牌图片
    private Character letter;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public Character getLetter() {
        return letter;
    }

    public void setLetter(Character letter) {
        this.letter = letter;
    }
}

BrandMapper类:

package com.leyou.item.mapper;

import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.common.Mapper;

import java.util.List;

public interface BrandMapper extends Mapper<Brand> {
    @Insert("insert into tb_category_brand(category_id, brand_id) values(#{cid},#{bid})")
    public abstract void saveCategoryAndBrand(@Param("cid")Long cid, @Param("bid")Long bid);

    @Select("select * from tb_brand where id in (select brand_id from tb_category_brand where category_id = #{cid})")
    public abstract List<Brand> queryByCid(Long cid);
}

一般来说,每张表对应一个实体类,也对应一个mapper用于操作这张表,但是在该表对应的mapper里也可以操作其它的表,这只是一种规范而已。
BrandController类:

package com.leyou.item.controller;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 根据指定条件返回查询结果
     * @param key
     * @param page
     * @param rows
     * @param sortBy
     * @param desc
     * @return
     */
    @GetMapping("page")
    public ResponseEntity<PageResult<Brand>> queryByPage(
            @RequestParam(value="key",required = false) String key,
            @RequestParam(value="page",defaultValue = "1") Integer page,
            @RequestParam(value="rows",defaultValue = "5") Integer rows,
            @RequestParam(value="sortBy",required = false) String sortBy,
            @RequestParam(value="desc",required = false) Boolean desc
    ){
        PageResult<Brand> pageResult = this.brandService.queryByPage(key, page, rows, sortBy, desc);
        if(CollectionUtils.isEmpty(pageResult.getItems())){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(pageResult);
    }

    /**
     * 保存一个新建的品牌
     * @param brand
     * @param cids
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> save(Brand brand, @RequestParam("cids") List<Long> cids){
        this.brandService.save(brand,cids);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    /**
     * 根据分类id查询所有品牌
     * @param cid
     * @return
     */
    @GetMapping("cid/{cid}")
    public ResponseEntity<List<Brand>> queryByCid(@PathVariable("cid") Long cid){
        List<Brand> brands = this.brandService.queryByCid(cid);
        if(CollectionUtils.isEmpty(brands)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(brands);
    }

    /**
     * 根据品牌id查询品牌
     * @param bid
     * @return
     */
    @GetMapping("{bid}")
    public ResponseEntity<Brand> queryByid(@PathVariable("bid") Long bid){
        Brand brand = this.brandService.queryById(bid);
        if(brand == null){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(brand);
    }
}
  • 请求方式:查询,肯定是Get
  • 请求路径:分页查询,/brand/page
  • 请求参数:根据我们刚才编写的页面,有分页功能,有排序功能,有搜索过滤功能,因此至少要有5个参数:
    • page:当前页,int
    • rows:每页大小,int
    • sortBy:排序字段,String
    • desc:是否为降序,boolean
    • key:搜索关键词,String
  • 响应结果:分页结果一般至少需要两个数据
    • total:总条数
    • items:当前页数据
    • totalPage:有些还需要总页数

这里我们封装一个类,来表示分页结果(注意该类在模块leyou-common下,因为别的模块也很有可能需要PageResult<>对象,因此将该类写在一个公用的模块下是非常合适的.):

package com.leyou.common.pojo;

import java.util.List;

public class PageResult<T> {
    private Long total;
    private Integer totalPage;
    private List<T> items;

    public PageResult(Long total, List<T> items) {
        this.total = total;
        this.items = items;
    }

    public PageResult(Long total, Integer totalPage, List<T> items) {
        this.total = total;
        this.totalPage = totalPage;
        this.items = items;
    }

    public PageResult() {
    }

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }

    public Integer getTotalPage() {
        return totalPage;
    }

    public void setTotalPage(Integer totalPage) {
        this.totalPage = totalPage;
    }

    public List<T> getItems() {
        return items;
    }

    public void setItems(List<T> items) {
        this.items = items;
    }
}

因此不要忘了引入依赖:

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

BrandServiceImpl类:

package com.leyou.item.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.pojo.Brand;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;

import java.util.List;

@Service
public class BrandServiceImpl implements BrandService {
    @Autowired
    private BrandMapper brandMapper;

    /**
     * 根据指定条件返回查询结果
     * @param key  // 根据key实现模糊查询
     * @param page //指定要查询哪一页
     * @param rows //指定每页显示多少条数据
     * @param sortBy //根据哪个字段排序
     * @param desc // 升序还是降序
     * @return
     */
    @Override
    public PageResult<Brand> queryByPage(String key, Integer page, Integer rows, String sortBy, Boolean desc) {
        Example example = new Example(Brand.class);
        Example.Criteria criteria = example.createCriteria();
        //加入查询条件
        if(!StringUtils.isEmpty(key)){
            criteria.andLike("name","%"+key+"%").orEqualTo("letter",key);
        }
        //设置分页
        PageHelper.startPage(page,rows);
        //设置排序方式
        if(!StringUtils.isEmpty(sortBy)){
            example.setOrderByClause(sortBy+" "+(desc?"desc":"asc"));
        }
        List<Brand> brands = this.brandMapper.selectByExample(example);
        PageInfo<Brand> pageInfo = new PageInfo<>(brands);
        return new PageResult<Brand>(pageInfo.getTotal(),pageInfo.getList());
    }

    /**
     * 新增一个品牌并同时更新品牌和类别的对应关系
     * @param brand
     * @param cids
     */
    @Override
    @Transactional
    public void save(Brand brand, List<Long> cids) {
        //先保存该品牌
        this.brandMapper.insertSelective(brand);
        //保存后id参数会注入到brand对象之中
        cids.forEach(cid -> this.brandMapper.saveCategoryAndBrand(cid,brand.getId()));
    }

    /**
     * 根据分类的id查找所有品牌
     * @param cid
     * @return
     */
    @Override
    public List<Brand> queryByCid(Long cid) {
        return this.brandMapper.queryByCid(cid);
    }

    /**
     * 根据品牌id查找相应的品牌
     * @param bid
     * @return
     */
    @Override
    public Brand queryById(Long bid) {
        return this.brandMapper.selectByPrimaryKey(bid);
    }
}

异步查询工具axios
异步查询数据,自然是通过ajax查询,之前我一直用的jQuery框架封装的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()方法的第二个参数对象,就是将来要传递的参数

PUT和DELETE请求与POST请求类似

axios的全局配置
http.js中对axios进行了一些默认配置:

import Vue from 'vue'
import axios from 'axios'
import config from './config'

axios.defaults.baseURL = config.api; // 设置axios的基础请求路径
axios.defaults.timeout = 2000; // 设置axios的请求时间

// axios.interceptors.request.use(function (config) {
//   // console.log(config);
//   return config;
// })

axios.loadData = async function (url) {
  const resp = await axios.get(url);
  return resp.data;
}

Vue.prototype.$http = axios;// 将axios添加到 Vue的原型,这样一切vue实例都可以使用该对象


  • http.js对axios进行了全局配置:baseURL=config.api,即http://api.leyou.com/api。因此以后所有用axios发起的请求,都会以这个地址作为前缀。
  • 通过Vue.property.$http = axios,将axios赋值给了 Vue原型中的$http。 这样以后所有的Vue实例都可以访问到$http,也就是访问到了axios了。

在页面端,如果需要与后端进行交互,直接利用axios发送相应的请求就可以了,这里直接贴上对应于品牌组件的前端页面(Brand.vue)代码:

<template>
  <v-card>
    <v-card-title>
      <v-btn color="primary" @click="addBrand">新增品牌</v-btn>
      <!--搜索框,与search属性关联-->
      <v-spacer/>
      <v-flex xs3>
      <v-text-field label="输入关键字搜索" v-model.lazy="search" append-icon="search" hide-details/>
      </v-flex>
    </v-card-title>
    <v-divider/>
    <v-data-table
      :headers="headers"
      :items="brands"
      :pagination.sync="pagination"
      :total-items="totalBrands"
      :loading="loading"
      class="elevation-1"
    >
      <template slot="items" slot-scope="props">
        <td class="text-xs-center">{{ props.item.id }}</td>
        <td class="text-xs-center">{{ props.item.name }}</td>
        <td class="text-xs-center">
          <img v-if="props.item.image" :src="props.item.image" width="130" height="40">
          <span v-else></span>
        </td>
        <td class="text-xs-center">{{ props.item.letter }}</td>
        <td class="justify-center layout px-0">
          <v-btn icon @click="editBrand(props.item)">
            <i class="el-icon-edit"/>
          </v-btn>
          <v-btn icon @click="deleteBrand(props.item)">
            <i class="el-icon-delete"/>
          </v-btn>
        </td>
      </template>
    </v-data-table>
    <!--弹出的对话框-->
    <v-dialog max-width="500" v-model="show" persistent scrollable>
      <v-card>
        <!--对话框的标题-->
        <v-toolbar dense dark color="primary">
          <v-toolbar-title>{{isEdit ? '修改' : '新增'}}品牌</v-toolbar-title>
          <v-spacer/>
          <!--关闭窗口的按钮-->
          <v-btn icon @click="closeWindow"><v-icon>close</v-icon></v-btn>
        </v-toolbar>
        <!--对话框的内容,表单-->
        <v-card-text class="px-5" style="height:400px">
          <brand-form @close="closeWindow" :oldBrand="oldBrand" :isEdit="isEdit"/>
        </v-card-text>
      </v-card>
    </v-dialog>
  </v-card>
</template>

<script>
  // 导入自定义的表单组件
  import BrandForm from './BrandForm'

  export default {
    name: "brand",
    data() {
      return {
        search: '', // 搜索过滤字段
        totalBrands: 0, // 总条数
        brands: [], // 当前页品牌数据
        loading: true, // 是否在加载中
        pagination: {}, // 分页信息
        headers: [
          {text: 'id', align: 'center', value: 'id'},
          {text: '名称', align: 'center', sortable: false, value: 'name'},
          {text: 'LOGO', align: 'center', sortable: false, value: 'image'},
          {text: '首字母', align: 'center', value: 'letter', sortable: true,},
          {text: '操作', align: 'center', value: 'id', sortable: false}
        ],
        show: false,// 控制对话框的显示
        oldBrand: {}, // 即将被编辑的品牌数据
        isEdit: false, // 是否是编辑
      }
    },
    mounted() { // 渲染后执行
      // 查询数据
      this.getDataFromServer();
    },
    watch: {
      pagination: { // 监视pagination属性的变化
        deep: true, // deep为true,会监视pagination的属性及属性中的对象属性变化
        handler() {
          // 变化后的回调函数,这里我们再次调用getDataFromServer即可
          this.getDataFromServer();
        }
      },
      search: { // 监视搜索字段
        handler() {
          this.getDataFromServer();
        }
      }
    },
    methods: {
      getDataFromServer() { // 从服务的加载数的方法。
        // 发起请求
        this.$http.get("/item/brand/page", {
          params: {
            key: this.search, // 搜索条件
            page: this.pagination.page,// 当前页
            rows: this.pagination.rowsPerPage,// 每页大小
            sortBy: this.pagination.sortBy,// 排序字段
            desc: this.pagination.descending// 是否降序
          }
        }).then(resp => { // 这里使用箭头函数
          this.brands = resp.data.items;
          this.totalBrands = resp.data.total;
          // 完成赋值后,把加载状态赋值为false
          this.loading = false;
        })
      },
      addBrand() {
        // 修改标记
        this.isEdit = false;
        // 控制弹窗可见:
        this.show = true;
        // 把oldBrand变为null
        this.oldBrand = null;
      },
      editBrand(oldBrand){
        // 根据品牌信息查询商品分类
        this.$http.get("/item/category/bid/" + oldBrand.id)
          .then(({data}) => {
            // 修改标记
            this.isEdit = true;
            // 控制弹窗可见:
            this.show = true;
            // 获取要编辑的brand
            this.oldBrand = oldBrand
            // 回显商品分类
            this.oldBrand.categories = data;
          })
      },
      closeWindow(){
        // 重新加载数据
        this.getDataFromServer();
        // 关闭窗口
        this.show = false;
      }
    },
    components:{
        BrandForm
    }
  }
</script>

<style scoped>

</style>

vue有一个神奇的监听机制,通过监听机制可以实现动态的分页查询和搜索过滤,很有意思。

8.品牌新增及fastDFS

8.1 品牌新增

品牌新增的页面又是一个新的组件(BrandForm.vue):

<template>
  <v-form v-model="valid" ref="myBrandForm">
    <v-text-field v-model="brand.name" label="请输入品牌名称" required :rules="nameRules"/>
    <v-text-field v-model="brand.letter" label="请输入品牌首字母" required :rules="letterRules"/>
    <v-cascader
      url="/item/category/list"
      multiple
      required
      v-model="brand.categories"
      label="请选择商品分类"/>
    <v-layout row>
      <v-flex xs3>
        <span style="font-size: 16px; color: #444">品牌LOGO</span>
      </v-flex>
      <v-flex>
        <v-upload
          v-model="brand.image" url="/upload/image" :multiple="false" :pic-width="250" :pic-height="90"
        />
      </v-flex>
    </v-layout>
    <v-layout class="my-4" row>
      <v-spacer/>
      <v-btn @click="submit" color="primary">提交</v-btn>
      <v-btn @click="clear">重置</v-btn>
    </v-layout>
  </v-form>
</template>

<script>
  export default {
    name: "brand-form",
    props: {
      oldBrand: {
        type: Object
      },
      isEdit: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        valid: false, // 表单校验结果标记
        brand: {
          name: '', // 品牌名称
          letter: '', // 品牌首字母
          image: '',// 品牌logo
          categories: [], // 品牌所属的商品分类数组
        },
        nameRules: [
          v => !!v || "品牌名称不能为空",
          v => v.length > 1 || "品牌名称至少2位"
        ],
        letterRules: [
          v => !!v || "首字母不能为空",
          v => /^[a-zA-Z]{1}$/.test(v) || "品牌字母只能是1个字母"
        ]
      }
    },
    methods: {
      submit() {
        // 表单校验
        if (this.$refs.myBrandForm.validate()) {
          // 定义一个请求参数对象,通过解构表达式来获取brand中的属性
          const {categories, letter, ...params} = this.brand;
          // 数据库中只要保存分类的id即可,因此我们对categories的值进行处理,只保留id,并转为字符串
          params.cids = categories.map(c => c.id).join(",");
          // 将字母都处理为大写
          params.letter = letter.toUpperCase();
          // 将数据提交到后台
          // this.$http.post('/item/brand', this.$qs.stringify(params))
          this.$http({
            method: this.isEdit ? 'put' : 'post',
            url: '/item/brand',
            data: this.$qs.stringify(params)
          }).then(() => {
            // 关闭窗口
            this.$emit("close");
            this.$message.success("保存成功!");
          })
            .catch(() => {
              this.$message.error("保存失败!");
            });
        }
      },
      clear() {
        // 重置表单
        this.$refs.myBrandForm.reset();
        // 需要手动清空商品分类
        this.categories = [];
      }
    },
    watch: {
      oldBrand: {// 监控oldBrand的变化
        handler(val) {
          if (val) {
            // 注意不要直接复制,否则这边的修改会影响到父组件的数据,copy属性即可
            this.brand = Object.deepCopy(val)
          } else {
            // 为空,初始化brand
            this.brand = {
              name: '',
              letter: '',
              image: '',
              categories: [],
            }
          }
        },
        deep: true
      }
    }
  }
</script>

<style scoped>

</style>

重置表单
重置表单相对简单,因为v-form组件已经提供了reset方法,用来清空表单数据。只要我们拿到表单组件对象,就可以调用方法了。

我们可以通过$refs内置对象来获取表单组件。

首先,在表单上定义ref属性:
在这里插入图片描述
我们在clear中来获取表单对象并调用reset方法:
在这里插入图片描述
要注意的是,需要手动把this.categories清空了,因为我写的级联选择组件并没有跟表单结合起来。
表单校验
Vuetify的表单校验,是通过rules属性来指定的:
校验规则的说明:

  • 规则是一个数组
  • 数组中的元素是一个函数,该函数接收表单项的值作为参数,函数返回值两种情况:
    • 返回true,代表成功,
    • 返回错误提示信息,代表失败

我们有四个字段:

  • name:做非空校验和长度校验,长度必须大于1
  • letter:首字母,校验长度为1,非空。
  • image:图片,不做校验,图片可以为空
  • categories:非空校验,自定义组件已经帮我们完成,不用写了

表单提交
在submit方法中添加表单提交的逻辑:

1.通过this.KaTeX parse error: Expected 'EOF', got '#' at position 478: …,t_70,g_se,x_16#̲pic_center) 这个插…message对象绑定到了Vue的原型上,因此我们可以通过this.$message来直接调用。

包含以下常用方法:

  • info、error、success、warning等,弹出一个带有提示信息的窗口,色调与为普通(灰)、错误(红色)、成功(绿色)和警告(黄色)。使用方法:this.$message.info(“msg”)
  • confirm:确认框。用法:this.$message.confirm(“确认框的提示信息”),返回一个Promise。

接下来是后台的编写,这部分我感觉还是比较简单的,代码上面已经贴过了,就不多叙述了。

不过这里还有一个地方值得注意,不然会报400(请求的参数不合法)错误:
axios处理请求体的原则会根据请求数据的格式来定:

  • 如果请求体是对象:会转为json发送
  • 如果请求体是String:会作为普通表单请求发送,但需要我们自己保证String的格式是键值对。
    如:name=jack&age=12

在这里插入图片描述
注意save方法的参数:一个是brand对象,另一个是一个类型为Long的id数组,如果前端页面传输的是一个js对象,那么会直接被转为json发送,那么接收端接收的时候就只能用一个对象来接收(如果该对象中没有对应的字段则会忽略),不能用第二个参数继续接收。为了解决这个问题,我们可以使用第三方库QS的stringify方法来讲json对象转为请求参数字符串,这样问题就解决了。

新增完成后关闭窗口
新增不管成功还是失败,窗口都一致在这里,不会关闭。这样很不友好,我们希望如果新增失败,窗口保持;但是新增成功,窗口关闭才对。因此,我们需要在新增的ajax请求完成以后,关闭窗口,这就需要在子组件中关闭窗口,但是控制窗口是否标记的参数在父组件中,这时候就需要用到父子组件之间的通信了。
第一步:在父组件中定义一个函数,用来关闭窗口,不过之前已经定义过了。父组件在使用子组件时,绑定事件,关联到这个函数:Brand.vue

<!--对话框的内容,表单-->
<v-card-text class="px-5" style="height:400px">
    <brand-form @close="closeWindow" :oldBrand="oldBrand" :isEdit="isEdit"/>
</v-card-text>

第二步,子组件通过this.$emit调用父组件的函数closeWindow:
在这里插入图片描述

8.2 图片上传

文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此我们创建一个独立的微服务,专门处理各种上传。
三步走:
1.引入文件上传微服务的依赖:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.upload</groupId>
    <artifactId>leyou-upload</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <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-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.tobato</groupId>
            <artifactId>fastdfs-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

2.更改配置:

server:
  port: 8082
spring:
  application:
    name: leyou-upload
  servlet:
    multipart:
      max-file-size: 5MB
eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka
  instance:
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 15
fdfs:
  so-timeout: 1501 # 超时时间
  connect-timeout: 601 # 连接超时时间
  thumb-image: # 缩略图
    width: 60
    height: 60
  tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)
    - 192.168.124.121:22122

3.编写引导类

package com.leyou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class LeyouUploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouUploadApplication.class,args);
    }
}

然后是controller类:
编写controller需要知道4个内容:

  • 请求方式:上传肯定是POST
  • 请求路径:/upload/image
  • 请求参数:文件,参数名是file,SpringMVC会封装为一个接口:MultipartFile
  • 返回结果:上传成功后得到的文件的url路径,也就是返回String
package com.leyou.upload.controller;

import com.leyou.upload.service.UploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("upload")
public class UploadController {
    @Autowired
    private UploadService uploadService;

    @PostMapping("image")
    public ResponseEntity<String> upload(@RequestParam("file")MultipartFile file, HttpServletRequest request){
        String path = this.uploadService.upload(file,request);
        if(StringUtils.isEmpty(path)){
            return ResponseEntity.badRequest().build();
        }
        return ResponseEntity.status(HttpStatus.CREATED).body(path);
    }
}

在上传文件过程中,我们需要对上传的内容进行校验:

  1. 校验文件大小
  2. 校验文件的媒体类型
  3. 校验文件的内容

文件大小在Spring的配置文件中设置,因此已经会被校验,我们不用管。

具体代码:

package com.leyou.upload.service.impl;

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.leyou.upload.service.UploadService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

@Service
public class UploadServiceImpl implements UploadService {
    private static final List<String> CONTENTTYPES = Arrays.asList("application/x-jpg","image/jpeg","application/x-png","application/x-bmp","image/png");
    private static Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);
    @Autowired
    private FastFileStorageClient storageClient;
    /**
     * 上传文件
     * @param file
     */
    @Override
    public String upload(MultipartFile file, HttpServletRequest request) {
        String originalFilename = file.getOriginalFilename();
        //System.out.println("file:"+file);
        //System.out.println("filename:"+originalFilename);
        //校验文件类型是否合法
        String contentType = file.getContentType();
        //System.out.println(contentType);
        if(!CONTENTTYPES.contains(contentType)){
            logger.info("{}的文件类型不合法",originalFilename);
            return null;
        }
        //校验文件内容是否合法
        BufferedImage bufferedImage = null;
        try {
            bufferedImage = ImageIO.read(file.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
        if(bufferedImage == null){
            logger.info("{}的文件内容不合法",originalFilename);
            return null;
        }
        //上传文件
        StorePath storePath = null;
        try {
            //file.transferTo(new File("D:\\data\\java-program\\IdeaProjects\\leyou\\leyou-upload\\src\\images",originalFilename));
            String last = StringUtils.substringAfterLast(originalFilename, ".");
            storePath = this.storageClient.uploadFile(file.getInputStream(), file.getSize(), last, null);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //返回文件的访问路径
        // return "http://www.image.leyou.com/"+originalFilename;
        return "http://image.leyou.com/"+storePath.getFullPath();
    }
}

这里有一个问题:为什么图片地址需要使用另外的url?

  • 图片不能保存在服务器内部,这样会对服务器产生额外的加载负担
  • 一般静态资源都应该使用独立域名,这样访问静态资源时不会携带一些不必要的cookie,减小请求的数据量

绕过网关
图片上传是文件的传输,如果也经过Zuul网关的代理,文件就会经过多次网路传输,造成不必要的网络负担。在高并发时,可能导致网络阻塞,Zuul网关不可用。这样我们的整个系统就瘫痪了。
所以,我们可以考虑让上传文件的请求绕过网关。
如何做到这一点呢?在nginx配置文件中进行如下配置即可:

	server {
        listen       80;
        server_name  api.leyou.com;

        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    	# 上传路径的映射
		location /api/upload {	
			proxy_pass http://127.0.0.1:8082;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
			
			rewrite "^/api/(.*)$" /$1 break; 
        }
		
        location / {
			proxy_pass http://127.0.0.1:10010;
			proxy_connect_timeout 600;
			proxy_read_timeout 600;
        }
    }
  • 首先,我们映射路径是/api/upload,而下面一个映射路径是 / ,根据最长路径匹配原则,/api/upload优先级更高。也就是说,凡是以/api/upload开头的路径,都会被第一个配置处理
  • proxy_pass:反向代理,这次我们代理到8082端口,也就是upload-service服务
  • rewrite “^/api/(.*)$” /$1 break,路径重写:
    • “^/api/(.*)$”:匹配路径的正则表达式,用了分组语法,把/api/以后的所有部分当做1组
    • /$1:重写的目标路径,这里用$1引用前面正则表达式匹配到的分组(组编号从1开始),即/api/后面的所有。这样新的路径就是除去/api/以外的所有,就达到了去除/api前缀的目的
    • break:指令,常用的有2个,分别是:last、break
      • last:重写路径结束后,将得到的路径重新进行一次路径匹配
      • break:重写路径结束后,不再重新匹配路径。
        我们这里不能选择last,否则以新的路径/upload/image来匹配,就不会被正确的匹配到8082端口了

修改完成,输入nginx -s reload命令重新加载配置。然后再次上传即可。
注意,由于我们越过了网关,所以这里又会出现跨域问题,因此需要加上一个配置类

8.3 FastDFS

上传本身没有任何问题,问题出在保存文件的方式,如果我们保存在服务器机器,就会有下面的问题:

  • 单机器存储,存储能力有限
  • 无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况
  • 数据没有备份,有单点故障风险
  • 并发能力差

这个时候,最好使用分布式文件存储来代替本地文件存储。

分布式文件系统
分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。

通俗来讲:

  • 传统文件系统管理的文件就存储在本机。

  • 分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理。无论是上传或者访问文件,都需要通过管理中心来访问
    FastDFS
    FastDFS是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统。用纯C语言开发,功能丰富:

  • 文件存储

  • 文件同步

  • 文件访问(上传、下载)

  • 存取负载均衡

  • 在线扩容

适合有大容量存储需求的应用或系统。同类的分布式文件系统有谷歌的GFS、HDFS(Hadoop)、TFS(淘宝)等。
FastDFS的架构图
在这里插入图片描述
FastDFS两个主要的角色:Tracker Server 和 Storage Server 。

  • Tracker Server:跟踪服务器,主要负责调度storage节点与client通信,在访问上起负载均衡的作用,和记录storage节点的运行状态,是连接client和storage节点的枢纽。
  • Storage Server:存储服务器,保存文件和文件的meta data(元数据),每个storage server会启动一个单独的线程主动向Tracker cluster中每个tracker server报告其状态信息,包括磁盘使用情况,文件同步情况及文件上传下载次数统计等信息
  • Group:文件组,多台Storage Server的集群。上传一个文件到同组内的一台机器上后,FastDFS会将该文件即时同步到同组内的其它所有机器上,起到备份的作用。不同组的服务器,保存的数据不同,而且相互独立,不进行通信。
  • Tracker Cluster:跟踪服务器的集群,有一组Tracker Server(跟踪服务器)组成。
  • Storage Cluster :存储集群,有多个Group组成。

上传和下载流程
在这里插入图片描述

  1. Client通过Tracker server查找可用的Storage server。
  2. Tracker server向Client返回一台可用的Storage server的IP地址和端口号。
  3. Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传。
  4. 上传完成,Storage server返回Client一个文件ID,文件上传结束。
    在这里插入图片描述
  5. Client通过Tracker server查找要下载文件所在的的Storage server。
  6. Tracker server向Client返回包含指定文件的某个Storage server的IP地址和端口号。
  7. Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并指定要下载文件。
  8. 下载文件成功。
    至于具体怎么使用,还是三步走的方式,比较特殊的是第三步,得添加如下的配置类:
package com.leyou.upload.config;

import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;

@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {

}

当然也可以编写一个测试方法:

package com.leyou.upload.test;

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.domain.ThumbImageConfig;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

@SpringBootTest
@RunWith(SpringRunner.class)
public class FastDFSTest {

    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private ThumbImageConfig thumbImageConfig;

    @Test
    public void testUpload() throws FileNotFoundException {
        // 要上传的文件
        File file = new File("C:\\Users\\admin\\Desktop\\hehe.png");
        // 上传并保存图片,参数:1-上传的文件流 2-文件的大小 3-文件的后缀 4-可以不管他
        StorePath storePath = this.storageClient.uploadFile(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
    }

    @Test
    public void testUploadAndCreateThumb() throws FileNotFoundException {
        File file = new File("C:\\Users\\admin\\Desktop\\hehe.png");
        // 上传并且生成缩略图
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
        // 获取缩略图路径
        String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
        System.out.println(path);
    }
}

最后是改造上传逻辑,代码已经在前面贴好。

最后记录一下修改品牌和删除品牌
修改品牌
首先在前端页面添加edit方法:

editBrand(oldBrand){
        // 根据品牌信息查询商品分类
        this.$http.get("/item/category/bid/" + oldBrand.id)
          .then(({data}) => {
            // 修改标记
            this.isEdit = true;
            // 控制弹窗可见:
            this.show = true;
            // 获取要编辑的brand
            this.oldBrand = oldBrand
            // 回显商品分类
            this.oldBrand.categories = data;
          })
      },

然后完成后台的实现:
BrandController:

/**
     * 对修改后的品牌进行保存
     * @param brand
     * @param cids
     * @return
     */
    @PutMapping
    public ResponseEntity<Void> update(Brand brand, @RequestParam("cids") List<Long> cids){
        this.brandService.update(brand,cids);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

BrandServiceImpl:

/**
     * 更新一个品牌并同时更新中间表的关系
     * @param brand
     * @param cids
     */
    @Override
    @Transactional
    public void update(Brand brand, List<Long> cids) {
        //更新品牌信息
        this.brandMapper.updateByPrimaryKey(brand);
        //先删除中间表中原有的对应关系
        this.brandMapper.deleteCategoryAndBrand(brand.getId());
        //然后添加新的关系
        cids.forEach(cid->this.brandMapper.saveCategoryAndBrand(cid,brand.getId()));
    }

注意更新中间表采用先删除再添加的方式
BrandMapper:

package com.leyou.item.mapper;

import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import tk.mybatis.mapper.common.Mapper;

import java.util.List;

public interface BrandMapper extends Mapper<Brand> {
    @Insert("insert into tb_category_brand(category_id, brand_id) values(#{cid},#{bid})")
    public abstract void saveCategoryAndBrand(@Param("cid")Long cid, @Param("bid")Long bid);

    @Select("select * from tb_brand where id in (select brand_id from tb_category_brand where category_id = #{cid})")
    public abstract List<Brand> queryByCid(Long cid);

    @Delete("delete from tb_category_brand where brand_id = #{id}")
    void deleteCategoryAndBrand(Long id);

}

删除品牌
删除品牌就比较简单了,同样先写前端页面,发送删除请求后记得重新请求数据:

//发送删除品牌的请求
      deleteBrand(brand){
        this.$http.delete("/item/brand/bid/"+brand.id)
        .then(
          ()=>{
            this.getDataFromServer();
          }
        ).catch();
      },

BrandController:

/**
     * 根据bid删除指定的品牌
     */
    @DeleteMapping("bid/{bid}")
    public ResponseEntity<Void> delete(@PathVariable("bid")Long bid){
        if(bid==null){
            ResponseEntity.badRequest().build();
        }
        this.brandService.delete(bid);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

BrandServiceImpl:

/**
     * 根据bid删除指定的品牌
     * @param bid
     */
    @Override
    @Transactional
    public void delete(Long bid) {
        Brand brand = new Brand();
        brand.setId(bid);
        this.brandMapper.deleteByPrimaryKey(brand);
        this.brandMapper.deleteCategoryAndBrand(bid);
    }

9.规格参数

9.1 商品规格数据结构

SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品

  • SPU是一个抽象的商品集概念,为了方便后台的管理。
  • SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU

不同的商品分类,可能属性是不一样的,比如手机有内存,衣服有尺码,这是一个全品类的电商网站,这些不同的商品的不同属性,如何设计到一张表中?其实颜色、内存、硬盘属性都是规格参数中的字段。所以,要解决这个问题,首先要能清楚规格参数。

虽然商品规格千变万化,但是同一类商品(如手机)的规格是统一的:
在这里插入图片描述
在这里插入图片描述
SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如华为META10的颜色、内存属性。不同种类的商品,一个手机,一个衣服,其SKU属性不相同。同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。SKU的特有属性也是商品规格参数的一部分。
也就是说,我们没必要单独对SKU的特有属性进行设计,它可以看做是规格参数中的一部分。这样规格参数中的属性可以标记成两部分

  • spu下所有sku共享的规格属性(称为全局属性)
  • 每个sku不同的规格属性(称为特有属性)

注意规格参数中的数据,将来会有一部分作为搜索条件来使用。我们可以在设计时,将这部分属性标记出来,将来做搜索的时候,作为过滤条件。要注意的是,无论是SPU的全局属性,还是SKU的特有属性,都有可能作为搜索过滤条件的,并不冲突,而是有一个交集
在这里插入图片描述
规格参数表
在这里插入图片描述
可以看到规格参数是分组的,每一组都有多个参数键值对。不过对于规格参数的模板而言,其值现在是不确定的,不同的商品值肯定不同,模板中只要保存组信息、组内参数信息即可
因此可以考虑设计两张表:

  • tb_spec_group:组,与商品分类关联
  • tb_spec_param:参数名,与组关联,一对多

规格参数分组表:tb_spec_group

CREATE TABLE `tb_spec_group` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
  `name` varchar(50) NOT NULL COMMENT '规格组的名称',
  PRIMARY KEY (`id`),
  KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';

规格组有3个字段:

  • id:主键
  • cid:商品分类id,一个分类下有多个模板
  • name:该规格组的名称。

规格参数表:tb_spec_param

CREATE TABLE `tb_spec_param` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `cid` bigint(20) NOT NULL COMMENT '商品分类id',
  `group_id` bigint(20) NOT NULL,
  `name` varchar(255) NOT NULL COMMENT '参数名',
  `numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
  `unit` varchar(255) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
  `generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
  `searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
  `segments` varchar(1000) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
  PRIMARY KEY (`id`),
  KEY `key_group` (`group_id`),
  KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';

我们的规格参数首先需要记录参数名、组id、商品分类id。另外,规格参数中有一部分是 SKU的通用属性,一部分是SKU的特有属性,而且其中会有一些将来用作搜索过滤,这些信息都需要标记出来。于是乎多了很多属性:
1.通用属性:用一个布尔类型字段来标记是否为通用:

  • generic来标记是否为通用属性:
    • true:代表通用属性
    • false:代表sku特有属性

2.搜索过滤:与搜索相关的有两个字段:

  • searching:标记是否用作过滤
    • true:用于过滤搜索
    • false:不用于过滤
  • segments:某些数值类型的参数,在搜索时需要按区间划分,这里提前确定好划分区间
    • 比如电池容量,02000mAh,2000mAh3000mAh,3000mAh~4000mAh

3.数值类型:某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:

  • numberic:是否为数值类型
    • true:数值类型
    • false:不是数值类型
  • unit:参数的单位

9.2 商品规格参数管理

这里完成规格参数组的查询,修改,增添和删除。
前端页面的分析过于复杂,这里就省略吧,后台的编写只涉及到查询,剩下的三部分有空再完成吧!
SpecGroup类:

package com.leyou.item.pojo;

import javax.persistence.*;
import java.util.List;

@Table(name = "tb_spec_group")
public class SpecGroup {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Long cid;

    private String name;

    @Transient //加上该注解表示忽略,因为数据库中没有该字段
    private List<SpecParam> params;

   // getter和setter省略

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCid() {
        return cid;
    }

    public void setCid(Long cid) {
        this.cid = cid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<SpecParam> getParams() {
        return params;
    }

    public void setParams(List<SpecParam> params) {
        this.params = params;
    }
}

SpecParam类:

package com.leyou.item.pojo;

import javax.persistence.*;

@Table(name = "tb_spec_param")
public class SpecParam {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long cid;
    private Long groupId;
    private String name;

    @Column(name = "`numeric`")
    private Boolean numeric; // numeric是mysql数据库中的一个关键字,必须加引号标识其不是关键字
    private String unit;
    private Boolean generic;
    private Boolean searching;
    private String segments;
    
    // getter和setter

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getCid() {
        return cid;
    }

    public void setCid(Long cid) {
        this.cid = cid;
    }

    public Long getGroupId() {
        return groupId;
    }

    public void setGroupId(Long groupId) {
        this.groupId = groupId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Boolean getNumeric() {
        return numeric;
    }

    public void setNumeric(Boolean numeric) {
        this.numeric = numeric;
    }

    public String getUnit() {
        return unit;
    }

    public void setUnit(String unit) {
        this.unit = unit;
    }

    public Boolean getGeneric() {
        return generic;
    }

    public void setGeneric(Boolean generic) {
        this.generic = generic;
    }

    public Boolean getSearching() {
        return searching;
    }

    public void setSearching(Boolean searching) {
        this.searching = searching;
    }

    public String getSegments() {
        return segments;
    }

    public void setSegments(String segments) {
        this.segments = segments;
    }
}

SpecificationController类:

package com.leyou.item.controller;

import com.leyou.item.pojo.SpecGroup;
import com.leyou.item.pojo.SpecParam;
import com.leyou.item.service.SpecificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
@RequestMapping("spec")
public class SpecificationController {
    @Autowired
    private SpecificationService specificationService;

    /**
     * 根据分类Id查询规格参数组
     * @param cid
     * @return
     */
    @GetMapping("groups/{cid}")
    public ResponseEntity<List<SpecGroup>> queryGroupsByCid(@PathVariable("cid") Long cid){
        List<SpecGroup> groups = this.specificationService.queryGroupsByCid(cid);
        if(CollectionUtils.isEmpty(groups)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(groups);
    }

    /**
     * 根据查询条件查询所有参数
     * @param gid
     * @param cid
     * @param generic
     * @param searching
     * @return
     */
    @GetMapping("params")
    public ResponseEntity<List<SpecParam>> queryParams(
            @RequestParam(value = "gid",required = false) Long gid,
            @RequestParam(value = "cid",required = false) Long cid,
            @RequestParam(value = "generic",required = false) Boolean generic,
            @RequestParam(value = "searching",required = false) Boolean searching){
        List<SpecParam> params = this.specificationService.queryParams(gid,cid,generic,searching);
        if(CollectionUtils.isEmpty(params)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(params);
    }

    /**
     * 根据cid查询所有参数组和组内所有的参数信息
     * @param cid
     * @return
     */
    @GetMapping("{cid}")
    public ResponseEntity<List<SpecGroup>> querySpecGroupsByCid(@PathVariable("cid") Long cid){
        List<SpecGroup> groups = this.specificationService.querySpecGroupsByCid(cid);
        if(CollectionUtils.isEmpty(groups)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(groups);
    }
}

SpecificationServiceImpl类:

package com.leyou.item.service.impl;

import com.leyou.item.mapper.SpecGroupMapper;
import com.leyou.item.mapper.SpecParamMapper;
import com.leyou.item.pojo.SpecGroup;
import com.leyou.item.pojo.SpecParam;
import com.leyou.item.service.SpecificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

@Service
public class SpecificationServiceImpl implements SpecificationService {
    @Autowired
    private SpecGroupMapper specGroupMapper;

    @Autowired
    private SpecParamMapper specParamMapper;

    /**
     * 根据cid查询所有参数组信息
     * @param cid
     * @return
     */
    @Override
    public List<SpecGroup> queryGroupsByCid(Long cid) {
        SpecGroup record = new SpecGroup();
        record.setCid(cid);
        return this.specGroupMapper.select(record);
    }

    /**
     * 根据查询条件查询所有参数信息
     * @param gid
     * @param cid
     * @param generic
     * @param searching
     * @return
     */
    @Override
    public List<SpecParam> queryParams(Long gid,Long cid,Boolean generic,Boolean searching) {
        SpecParam record = new SpecParam();
        record.setGroupId(gid);
        record.setCid(cid);
        record.setSearching(searching);
        record.setGeneric(generic);
        return this.specParamMapper.select(record);
    }

    /**
     * 根据cid查询所有参数组信息和组内所有的参数信息
     * @param cid
     * @return
     */
    @Override
    public List<SpecGroup> querySpecGroupsByCid(Long cid) {
        List<SpecGroup> specGroups = this.queryGroupsByCid(cid);
        return specGroups.stream().map(specGroup -> {
            List<SpecParam> params = this.queryParams(specGroup.getId(), null, null, null);
            specGroup.setParams(params);
            return specGroup;
        }).collect(Collectors.toList());
    }

}

9.3 SPU和SKU数据结构

SPU表:

CREATE TABLE `tb_spu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
  `title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
  `sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
  `cid1` bigint(20) NOT NULL COMMENT '1级类目id',
  `cid2` bigint(20) NOT NULL COMMENT '2级类目id',
  `cid3` bigint(20) NOT NULL COMMENT '3级类目id',
  `brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
  `saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
  `valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
  `create_time` datetime DEFAULT NULL COMMENT '添加时间',
  `last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';

将表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail:

CREATE TABLE `tb_spu_detail` (
  `spu_id` bigint(20) NOT NULL,
  `description` text COMMENT '商品描述信息',
  `generic_spec` varchar(10000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
  `special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
  `packing_list` varchar(3000) DEFAULT '' COMMENT '包装清单',
  `after_service` varchar(3000) DEFAULT '' COMMENT '售后服务',
  PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。需要注意的是这两个字段:generic_spec和special_spec。一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数信息,因此我们计划是这样:

  • SPUDetail中保存通用的规格参数信息。
  • SKU中保存特有规格参数。

来看下我们的表如何存储这些信息。
generic_spec字段
首先是generic_spec,其中保存通用规格参数信息的值,这里为了方便查询,使用了json格式:
在这里插入图片描述
json结构,其中都是键值对:

  • key:对应的规格参数的spec_param的id
  • value:对应规格参数的值

special_spec字段
我们说spu中只保存通用规格参数,那么为什么有多出了一个special_spec字段呢?
以手机为例,品牌、操作系统等肯定是全局通用属性,内存、颜色等肯定是特有属性。
当你确定了一个SPU,比如小米的:红米4X
全局属性值都是固定的了:

品牌:小米
型号:红米4X

特有属性举例:

颜色:[香槟金, 樱花粉, 磨砂黑]
内存:[2G, 3G]
机身存储:[16GB, 32GB]

颜色、内存、机身存储,作为SKU特有属性,key虽然一样,但是SPU下的每一个SKU,其值都不一样,所以值会有很多,形成数组。
我们在SPU中,会把特有属性的所有值都记录下来,形成一个数组:
在这里插入图片描述

也是json结构:

  • key:规格参数id
  • value:spu属性的数组

那么问题来:特有规格参数应该在sku中记录才对,为什么在spu中也要记录一份?
因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:
在这里插入图片描述
在spu中也记录一份会使页面渲染非常方便。

sku表

CREATE TABLE `tb_sku` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
  `spu_id` bigint(20) NOT NULL COMMENT 'spu id',
  `title` varchar(255) NOT NULL COMMENT '商品标题',
  `images` varchar(1000) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
  `price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
  `indexes` varchar(100) COMMENT '特有规格属性在spu属性模板中的对应下标组合',
  `own_spec` varchar(1000) COMMENT 'sku的特有规格参数,json格式',
  `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
  `create_time` datetime NOT NULL COMMENT '添加时间',
  `last_update_time` datetime NOT NULL COMMENT '最后修改时间',
  PRIMARY KEY (`id`),
  KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的64GB的iphone 8';

还有一张表,代表库存:

CREATE TABLE `tb_stock` (
  `sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',
  `seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',
  `seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',
  `stock` int(9) NOT NULL COMMENT '库存数量',
  PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';

问题:为什么要将库存独立一张表?
因为库存字段写频率较高,而SKU的其它字段以读为主,因此我们将两张表分离,读写不会干扰。

特别需要注意的是sku表中的indexes字段和own_spec字段。sku中应该保存特有规格参数的值,就在这两个字段中。

9.4 商品查询

简单来说,就是完成如下的页面:
在这里插入图片描述
这部分的业务逻辑很复杂,具体的就不复习了,直接贴上写好的代码吧!
Spu实体类:

package com.leyou.item.pojo;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Table(name = "tb_spu")
public class Spu {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long brandId;
    private Long cid1;// 1级类目
    private Long cid2;// 2级类目
    private Long cid3;// 3级类目
    private String title;// 标题
    private String subTitle;// 子标题
    private Boolean saleable;// 是否上架
    private Boolean valid;// 是否有效,逻辑删除用
    private Date createTime;// 创建时间
    private Date lastUpdateTime;// 最后修改时间
	// 省略getter和setter

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getBrandId() {
        return brandId;
    }

    public void setBrandId(Long brandId) {
        this.brandId = brandId;
    }

    public Long getCid1() {
        return cid1;
    }

    public void setCid1(Long cid1) {
        this.cid1 = cid1;
    }

    public Long getCid2() {
        return cid2;
    }

    public void setCid2(Long cid2) {
        this.cid2 = cid2;
    }

    public Long getCid3() {
        return cid3;
    }

    public void setCid3(Long cid3) {
        this.cid3 = cid3;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSubTitle() {
        return subTitle;
    }

    public void setSubTitle(String subTitle) {
        this.subTitle = subTitle;
    }

    public Boolean getSaleable() {
        return saleable;
    }

    public void setSaleable(Boolean saleable) {
        this.saleable = saleable;
    }

    public Boolean getValid() {
        return valid;
    }

    public void setValid(Boolean valid) {
        this.valid = valid;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getLastUpdateTime() {
        return lastUpdateTime;
    }

    public void setLastUpdateTime(Date lastUpdateTime) {
        this.lastUpdateTime = lastUpdateTime;
    }
}

Sku实体类:

package com.leyou.item.pojo;

import javax.persistence.*;
import java.util.Date;

@Table(name = "tb_sku")
public class Sku {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long spuId;
    private String title;
    private String images;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getSpuId() {
        return spuId;
    }

    public void setSpuId(Long spuId) {
        this.spuId = spuId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImages() {
        return images;
    }

    public void setImages(String images) {
        this.images = images;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    public String getOwnSpec() {
        return ownSpec;
    }

    public void setOwnSpec(String ownSpec) {
        this.ownSpec = ownSpec;
    }

    public String getIndexes() {
        return indexes;
    }

    public void setIndexes(String indexes) {
        this.indexes = indexes;
    }

    public Boolean getEnable() {
        return enable;
    }

    public void setEnable(Boolean enable) {
        this.enable = enable;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    public Date getLastUpdateTime() {
        return lastUpdateTime;
    }

    public void setLastUpdateTime(Date lastUpdateTime) {
        this.lastUpdateTime = lastUpdateTime;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    private Long price;
    private String ownSpec;// 商品特殊规格的键值对
    private String indexes;// 商品特殊规格的下标
    private Boolean enable;// 是否有效,逻辑删除用
    private Date createTime;// 创建时间
    private Date lastUpdateTime;// 最后修改时间
    @Transient
    private Integer stock;// 库存
}

注意由于商品查询页面包含了Spu中不具备的关键字,因此我们需要创建一个新的类SpuBo来继承现有的Spu,而不能直接修改Spu类,因为别的微服务可能使用了Spu类,如果此时修改Spu,那么将会产生大范围的影响,所以新建一个类来继承原有类才是一个可行的解决办法。
SpuBo类:

package com.leyou.item.bo;

import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;

import java.util.List;

public class SpuBo extends Spu {
    private String cname;
    private String bname;
    private SpuDetail spuDetail;
    private List<Sku> skus;

    public SpuDetail getSpuDetail() {
        return spuDetail;
    }

    public void setSpuDetail(SpuDetail spuDetail) {
        this.spuDetail = spuDetail;
    }

    public List<Sku> getSkus() {
        return skus;
    }

    public void setSkus(List<Sku> skus) {
        this.skus = skus;
    }

    public String getCname() {
        return cname;
    }

    public void setCname(String cname) {
        this.cname = cname;
    }

    public String getBname() {
        return bname;
    }

    public void setBname(String bname) {
        this.bname = bname;
    }
}

GoodsController类:

package com.leyou.item.controller;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.SpuBo;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
public class GoodsController {
    @Autowired
    private GoodsService goodsService;

    /**
     * 根据条件分页查询spu
     * @param key
     * @param saleable
     * @param page
     * @param rows
     * @return
     */
    @GetMapping("spu/page")
    public ResponseEntity<PageResult<SpuBo>> queryByPage(
            @RequestParam(value = "key",required = false) String key,
            @RequestParam(value = "saleable",required = false) Boolean saleable,
            @RequestParam(value = "page",defaultValue = "1") Integer page,
            @RequestParam(value = "rows",defaultValue = "5") Integer rows
    ){
        PageResult<SpuBo> pageResult = this.goodsService.queryByPage(key,saleable,page,rows);
        if(pageResult == null || CollectionUtils.isEmpty(pageResult.getItems())){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(pageResult);
    }

    /**
     *根据接收到的SpuBo数据更新数据库的相应表:tb_spu,tb_spu_detail,tb_stock,tb_sku
     */
    @PostMapping("goods")
    public ResponseEntity<Void> saveGoods(@RequestBody SpuBo spuBo){
        this.goodsService.saveGoods(spuBo);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    /**
     * 根据spuid查找对应的spu_detail
     * @param spuId
     * @return
     */
    @GetMapping("spu/detail/{spuId}")
    public ResponseEntity<SpuDetail> querySpuDetailBySpuId(@PathVariable("spuId") Long spuId){
        SpuDetail spuDetail = this.goodsService.querySpuDetailBySpuId(spuId);
        if(spuDetail == null){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(spuDetail);
    }

    /**
     * 根据spuid查找所有的sku
     * @param spuId
     * @return
     */
    @GetMapping("sku/list")
    public ResponseEntity<List<Sku>> querySkusBySpuId(@RequestParam("id") Long spuId){
        List<Sku> skus = this.goodsService.querySkusBySpuId(spuId);
        if(CollectionUtils.isEmpty(skus)){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(skus);
    }

    /**
     * 更新商品信息
     * @param spuBo
     */
    @PutMapping("goods")
    public ResponseEntity<Void> upateGoods(@RequestBody SpuBo spuBo){
        this.goodsService.updateGoods(spuBo);
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }

    /**
     * 根据spuId查询spu
     */
    @GetMapping("{id}")
    public ResponseEntity<Spu> querySpuBySpuId(@PathVariable("id") Long spuId){
        Spu spu = this.goodsService.querySpuBySpuId(spuId);
        if(spu == null){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(spu);
    }

    /**
     * 根据skuId查询sku
     * @param skuId
     * @return
     */
    @GetMapping("sku/{skuId}")
    public ResponseEntity<Sku> querySkuByskuId(@PathVariable("skuId") Long skuId){
        Sku sku = this.goodsService.querySkuByskuId(skuId);
        if(sku == null){
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(sku);
    }
}

GoodsServiceImpl类:

package com.leyou.item.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.SpuBo;
import com.leyou.item.mapper.*;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import com.leyou.item.pojo.Stock;
import com.leyou.item.service.CategoryService;
import com.leyou.item.service.GoodsService;
import com.netflix.discovery.converters.Auto;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.apache.commons.lang.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class GoodsServiceImpl implements GoodsService {
    @Autowired
    private SpuMapper spuMapper;

    @Autowired
    private BrandMapper brandMapper;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private SpuDetailMapper spuDetailMapper;

    @Autowired
    private SkuMapper skuMapper;

    @Autowired
    private StockMapper stockMapper;

    @Autowired
    private AmqpTemplate amqpTemplate;
    /**
     * 根据条件分页查询spu
     * @param key
     * @param saleable
     * @param page
     * @param rows
     * @return
     */
    @Override
    public PageResult<SpuBo> queryByPage(String key, Boolean saleable, Integer page, Integer rows) {
        Example example = new Example(Spu.class);
        Example.Criteria criteria = example.createCriteria();
        // 指定模糊查询
        if(!StringUtils.isEmpty(key)){
            criteria.andLike("title","%"+key+"%");
        }
        // 指定过滤条件
        if(saleable != null){
            criteria.andEqualTo("saleable",saleable);
        }
        // 使用分页查询
        PageHelper.startPage(page,rows);
        List<Spu> spus = this.spuMapper.selectByExample(example);
        PageInfo<Spu> pageInfo = new PageInfo<>(spus);
        // 查询结果处理后再封装
        List<SpuBo> spubos = spus.stream().map(spu -> {
            SpuBo spuBo = new SpuBo();
            BeanUtils.copyProperties(spu,spuBo);
            // 设置剩余的参数值
            // 设置品牌名称
            spuBo.setBname(this.brandMapper.selectByPrimaryKey(spu.getBrandId()).getName());
            // 设置商品分类的名称
            List<Long> ids = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3());
            List<String> strings = this.categoryService.queryByIds(ids).stream().map(category -> category.getName()).collect(Collectors.toList());
            spuBo.setCname(StringUtils.join(strings,"-"));
            return spuBo;
        }).collect(Collectors.toList());
        // 返回需要的pageResult对象
        return new PageResult<SpuBo>(pageInfo.getTotal(),spubos);
    }

    /**
     * 将传递过来的商品信息保存到数据库之中
     * @param spuBo
     */
    @Override
    @Transactional
    public void saveGoods(SpuBo spuBo) {
        // 保存spu
        spuBo.setId(null);
        spuBo.setSaleable(true);
        spuBo.setValid(true);
        spuBo.setCreateTime(new Date());
        spuBo.setLastUpdateTime(spuBo.getCreateTime());
        this.spuMapper.insertSelective(spuBo);
        // 保存spu_detail
        SpuDetail spuDetail = spuBo.getSpuDetail();
        spuDetail.setSpuId(spuBo.getId());
        this.spuDetailMapper.insertSelective(spuDetail);

        saveSkuAndStock(spuBo);

        //保存后将消息传递给消息队列,如果发送消息的过程出了问题,则应该手动处理,不要因为消息发送不成功而触发了事务,因为保存数据不应该受到发送消息的影响
        //已经在配置中指定了默认的交换机: LEYOU_ITEM_EXCHANGE
        try {
            sendMessage("insert",spuBo.getId());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 工作函数,根据指定的routingKey和要发送的消息这两个数据来发送消息
     * @param type
     * @param id
     */
    private void sendMessage(String type,Long id){
        //已经在配置中指定了默认的交换机: LEYOU_ITEM_EXCHANGE
        this.amqpTemplate.convertAndSend("item."+type, id);
    }

    /**
     * 根据spuid查找对应的spu_detail
     * @param spuId
     * @return
     */
    @Override
    public SpuDetail querySpuDetailBySpuId(Long spuId) {
        return this.spuDetailMapper.selectByPrimaryKey(spuId);
    }

    /**
     * 根据spuid查找所有的sku
     * @param spuId
     * @return
     */
    @Override
    public List<Sku> querySkusBySpuId(Long spuId) {
        Sku record = new Sku();
        record.setSpuId(spuId);
        List<Sku> skus = this.skuMapper.select(record);
        skus.forEach(sku -> {
            Stock stock = this.stockMapper.selectByPrimaryKey(sku.getId());
            sku.setStock(stock.getStock());
        });
        return skus;
    }

    /**
     * 更新商品信息
     * @param spuBo
     */
    @Override
    @Transactional
    public void updateGoods(SpuBo spuBo) {
        // 删除stock
        List<Sku> skus = spuBo.getSkus();
        skus.forEach(sku -> this.stockMapper.deleteByPrimaryKey(sku.getId()));
        // 删除sku
        Sku record = new Sku();
        record.setSpuId(spuBo.getId());
        this.skuMapper.delete(record);
        // 添加sku
        // 添加stock
        saveSkuAndStock(spuBo);
        // 更新spu和spu_detail
        // spuBo.setCreateTime(null);
        spuBo.setLastUpdateTime(new Date());
        //spuBo.setSaleable(null);  设置为null值依然会将其更新为Null值,如何设置null值的字段不更新?
        //spuBo.setValid(null);
        this.spuMapper.updateByPrimaryKey(spuBo);
        this.spuDetailMapper.updateByPrimaryKey(spuBo.getSpuDetail());

        //发送消息
        try {
            sendMessage("update",spuBo.getId());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据spuId查询spu
     * @param spuId
     * @return
     */
    @Override
    public Spu querySpuBySpuId(Long spuId) {
        Spu spu = this.spuMapper.selectByPrimaryKey(spuId);
        return spu;
    }

    /**
     * 根据skuId查询sku
     * @param skuId
     * @return
     */
    @Override
    public Sku querySkuByskuId(Long skuId) {
        return this.skuMapper.selectByPrimaryKey(skuId);
    }

    /**
     * 工作函数,用于将spuBo对象中的数据保存到sku和stock表之中
     * @param spuBo
     */
    private void saveSkuAndStock(SpuBo spuBo) {
        List<Sku> skus = spuBo.getSkus();
        skus.forEach(sku -> {
            // 保存sku与stock
            sku.setId(null);
            sku.setSpuId(spuBo.getId());
            sku.setCreateTime(new Date());
            sku.setLastUpdateTime(sku.getCreateTime());
            this.skuMapper.insertSelective(sku);
            Stock stock = new Stock();
            stock.setSkuId(sku.getId());
            stock.setStock(sku.getStock());
            this.stockMapper.insertSelective(stock);
        });
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值