谷粒商城项目学习笔记三

谷粒商城项目学习笔记三

一、商品服务

1.1、分类维护

1、递归树形结构获取数据(P45)

1、导入三级分类数据尚硅谷谷粒商城电商项目\1.分布式基础(全栈开发篇)\docs\代码\sql\pms_catelog.sql

2、修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:

    @Autowired
    private CategoryService categoryService;

    /**
     * 查出所有分类及子分类,以树形结构组装起来
     */
    @RequestMapping("/list/tree")
    public R list(){

        List<CategoryEntity> entities = categoryService.listWithTree();

        return R.ok().put("data", entities);
    }

3、修改‘com.atguigu.gulimall.product.service.CategoryService’类,代码如下

    List<CategoryEntity> listWithTree();

4、如何区别是哪种分类级别?

答:可以通过分类的parent_cid来进行判断,如果是一级分类,其值为0.

修改‘com.atguigu.gulimall.product.service.impl.CategoryServiceImpl’类,代码如下

    @Override
    public List<CategoryEntity> listWithTree() {
        //1、查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2、组装成父子的树形结构
        //2.1、找到所有一级分类
        List<CategoryEntity> level1Menus = entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .map(menu->{
                    menu.setChildren(getChildren(menu,entities));
                    return menu;
                })
                .sorted((menu1,menu2)->{
                    return (menu1.getSort()==null?0: menu1.getSort())-(menu2.getSort()==null?0: menu2.getSort());
                })
                .collect(Collectors.toList());

        return level1Menus;
    }

    //递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){
        List<CategoryEntity> children = all.stream()
                .filter(e -> e.getParentCid() == root.getCatId())
                .map(menu->{
                    //1、找到子菜单
                    menu.setChildren(getChildren(menu,all));
                    return menu;
                })
                //2、菜单的排序
                .sorted((menu1,menu2)->{
                    return (menu1.getSort()==null?0: menu1.getSort())-(menu2.getSort()==null?0: menu2.getSort());
                })
                .collect(Collectors.toList());
        return children;
    }

5、启动商品服务,测试:http://localhost:10000/product/category/list/tree

部分数据:

[{catId: 1, name: "图书、音像、电子书刊", parentCid: 0, catLevel: 1, showStatus: 1, sort: 0, icon: null,…},…]

2、三级分类前端实现(P46)

启动后端项目renren-fast

启动前端项目renren-fast-vue:

npm run dev

创建目录:商品系统和一级菜单:分类维护:

谷粒商城项目学习笔记三_第1张图片

创建成功后:

谷粒商城项目学习笔记三_第2张图片

打开RENREN-FAST-VUE前端代码,创建renren-fast-vue\src\views\modules\product目录,之所以是这样来创建,是因为product/category,对应于product-category

在该目录下,新建“category.vue”文件,使用vue模板创建:

<template>
    <el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
//import 引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
    menus: [],
       defaultProps: {
        children: "children",
        label: "name"
      },
};
},
//计算属性 类似于 data 概念
computed: {},
//监控 data 中的数据变化
watch: {},
//方法集合
methods: {

    handleNodeClick(data) {
      console.log(data);
    },
 
    getMenus() {
      this.dataListLoading = true;
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get"
      }).then(({ data }) => {
        console.log("获取到数据", data);
        this.menus=data;
      });
    }
},
//生命周期 - 创建完成(可以访问当前 this 实例)
created() {  
    this.getMenus();
},
//生命周期 - 挂载完成(可以访问 DOM 元素)
mounted() {

},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有 keep-alive 缓存功能,这个函数会触发
}
</script>
<style lang='scss' scoped>
//@import url(); 引入公共 css 类

</style>

3、出现错误

刷新页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”

这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree,

修正这个问题:

替换“static\config\index.js”文件中的“window.SITE_CONFIG[‘baseUrl’]”

替换前:

window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';

替换后:

window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

http://localhost:88,这个地址是我们网关微服务的接口。

但是这样做也引入了另外的一个问题,再次访问:http://localhost:8001/#/login,发现验证码不再显示:

分析原因:

现在的验证码请求路径为,http://localhost:88/captcha.jpg
原始的验证码请求路径:http://localhost:8080/renren-fast/captcha.jpg

在renren-fast模块的pom.xml中引入依赖:

		<dependency>
			<groupId>com.alibaba.cloudgroupId>
			<artifactId>spring-cloud-alibaba-nacos-discoveryartifactId>
			<version>2.1.0.RELEASEversion>
		dependency>

这里我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心

application:
    name: renren-fast
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
 
      config:
        name: renren-fast
        server-addr: 127.0.0.1.8848
        namespace: ee409c3f-3206-4a3b-ba65-7376922a886d

配置网关路由,前台的所有请求都是经由“http://localhost:88/api”来转发的,在“gulimall-gateway”中添加路由规则:

 - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**

在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写

关于请求路径重写:

6.16. The RewritePath GatewayFilter Factory

The RewritePath GatewayFilter factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path. The following listing configures a RewritePath GatewayFilter:

Example 41. application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: rewritepath_route
        uri: https://example.org
        predicates:
        - Path=/foo/**
        filters:
        - RewritePath=/red(?>/?.*), $\{segment}

For a request path of /red/blue, this sets the path to /blue before making the downstream request. Note that the $ should be replaced with $\ because of the YAML specification.

修改“admin_route”路由规则:

        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?>/?.*),/renren-fast/$\{segment}

再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。

4、解决跨域问题

但是很不幸新的问题又产生了,访问被拒绝了

image-20200425192722821

问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。

问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制

谷粒商城项目学习笔记三_第3张图片

跨域流程:

谷粒商城项目学习笔记三_第4张图片

谷粒商城项目学习笔记三_第5张图片

谷粒商城项目学习笔记三_第6张图片

解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。

@Configuration
public class GulimallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter(){
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration config = new CorsConfiguration();

        //1、配置跨域
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.setAllowCredentials(true);

        source.registerCorsConfiguration("/**",config);

        return new CorsWebFilter(source);
    }
}

http://localhost:8001/renren已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg

出现了多个请求,并且也存在多个跨源请求。

为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。

@Configuration
public class CorsConfig implements WebMvcConfigurer {

//    @Override
//    public void addCorsMappings(CorsRegistry registry) {
//        registry.addMapping("/**")
//            .allowedOrigins("*")
//            .allowCredentials(true)
//            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
//            .maxAge(3600);
//    }
}

再次运行:http://localhost:8001/#/login,OK了

在显示分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在

谷粒商城项目学习笔记三_第7张图片

这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:88/renren-fast/product/category/list/tree

但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

解决方法就是定义一个product路由规则,进行路径重写:

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?>/?.*),/$\{segment}

在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。

同时在gulimall-product目录的resources目录下新建bootstrap.properties配置文件

spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=10f5ddf4-4255-424f-a545-2f252a0d1f0b

重启product模块,分类信息就不会报错了。

5、树形显示分类数据

修改category.vue





保存后自动运行,就可以正确显示三级分类数据了

谷粒商城项目学习笔记三_第8张图片

6、删除-页面效果

VSCode快捷键:CTRL+D:删除一行

​ ALT+SHIFT+F:格式化代码

​ ALT+SHIFT+上箭头/下箭头:向上/向下拷贝一行

​ ALT+上箭头/下箭头:向上/向下移动一行

添加delete和append标识,并且增加复选框

 <el-tree
    :data="menus"
    show-checkbox  //显示复选框
    :props="defaultProps"  
    :expand-on-click-node="false" //设置节点点击时不展开
    node-key="catId"   
  >
    <span class="custom-tree-node" slot-scope="{ node, data }">
      <span>{{ node.label }}</span>
      <span>
        <el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">Append</el-button>
        <el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">Delete</el-button>
      </span>
    </span>
  </el-tree>

测试删除数据,打开postman输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,由于delete请求接收的是一个数组,所以这里使用JSON方式,传入了一个数组:

image-20200426113003531

再次查询数据库能够看到cat_id为1000的数据已经被删除了,说明逆向生成的代码是OK的。但是真正的删除没有这么简单。

修改“com.atguigu.gulimall.product.controller.CategoryController”类,添加如下代码:

/**
     * 删除
     * @RequestBody:获取请求体,必须发送post请求
     * springMVC自动将请求体的数据(json),转为对应的对象
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] catIds){
        //检查当前删除的菜单,是否被别的地方引用
		//categoryService.removeByIds(Arrays.asList(catIds));
 
		categoryService.removeMenusByIds(Arrays.asList(catIds));
        return R.ok();
    }

com.atguigu.gulimall.product.service.impl.CategoryServiceImpl

@Override
    public void removeMenusByIds(List<Long> asList) {
        //TODO  检查当前删除的菜单,是否被别的地方引用
        //逻辑删除
        baseMapper.deleteBatchIds(asList);
    }

然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;

可以设置show_status为0,标记它已经被删除。

7、逻辑删除

IDEA快捷键:CTRL+N或者双击SHIFT:打开全局搜索

mybatis-plus的逻辑删除:

https://baomidou.com/pages/6b03c5/

image-20200426115420393

配置全局的逻辑删除规则,在“src/main/resources/application.yml”文件中添加如下内容:

mybatis-plus:
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

修改“com.atguigu.gulimall.product.entity.CategoryEntity”类,添加上@TableLogic,表明使用逻辑删除:

	/**
	 * 是否显示[0-不显示,1显示]
	 */
	@TableLogic(value = "1",delval = "0")
	private Integer showStatus;

然后在Postman中测试一下是否能够满足需要。另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:

logging:
  level:
    com.bigdata.gulimall.product: debug

打印的日志:

 ==>  Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1 
 ==> Parameters: 1431(Long)
 <==    Updates: 1
 get changedGroupKeys:[]

8、菜单拖动

同一个菜单内拖动 正常
拖动到父菜单的前面或后面 正常
拖动到父菜单同级的另外一个菜单中 正常

关注的焦点在于,拖动到目标节点中,使得目标节点的catlevel+deep小于3即可。

需要考虑两种类型节点的catLevel

一种关系是:如果是同一个节点下的子节点的前后移动,则不需要修改其catLevel

如果是拖动到另外一个节点内或父节点中,则要考虑修改其catLevel

如果拖动到与父节点平级的节点关系中,则要将该拖动的节点的catLevel,设置为兄弟节点Level,

先考虑parentCid还是先考虑catLevel?

两种关系在耦合

另外还有一种是前后拖动的情况

哪个范围最大?

肯定是拖动类型关系最大,

如果是前后拖动,则拖动后需要看待拖动节点的层级和设置待拖动节点的parentId,

如果待拖动节点和目标节点的层级相同,则认为是同级拖动,只需要修改节点的先后顺序即可;

否则认为是跨级拖动,则需要修改层级和重新设置parentID

如果以拖动类型来分,并不合适,比较合适的是跨级拖动和同级拖动

如何判断是跨级拖动还是同级拖动,根据拖动的层级来看,如果是同一级的拖动,只需要修改先后顺序即可,但是这样也会存在一个问题,就是当拖动到另外一个分组下的同级目录中,显然也需要修改parentID,究竟什么样的模型最好呢?

另外也可以判断在跨级移动时,跨级后的parentID是否相同,如果不相同,则认为是在不同目录下的跨级移动需要修改parentID。

顺序、catLevel和parentID

同级移动:

(1)首先判断待移动节点和目标节点的catLevel是否相同,

(2)相同则认为是同级移动,

如果此时移动后目标节点的parentID和待移动节点的相同,但是移动类型是前后移动,只需要调整顺序即可,此时移动类型是inner,则需要修改catLevel和parentId和顺序

如果此时移动后目标节点的parentID和待移动节点的不相同,但是移动类型是前后移动,则需要调整顺序和parentId,此时移动类型是inner,则需要修改catLevel和parentId和顺序

通过这两步的操作能看到一些共性,如果抽取移动类型作为大的分类,则在这种分类下,

如果是前后移动,则分为下面几种情况:

同级别下的前后移动:界定标准为catLevel相同,但是又可以分为parentID相同和parentID不同,parent相同时,只需要修改顺序即可;parentID不同时,需要修改parentID和顺序

不同级别下的前后移动:界定标准为catLevel不同,此时无论如何都要修改parentID,顺序和catLevel

如果是inner类型移动,则分为一下的几种情况。

此时不论是同级inner,还是跨级innner,都需要修改parentID,顺序和catLevel

哪种情况需要更新子节点呢?

那就要看要拖拽的节点是否含有子节点,如果有子节点,则需要更新子节点的catLevel,不需要更新它之间的顺序和parentId,只需要更新catLevel即可。这种更新子节点的Level应该归类,目前的目标是只要有子节点就更新它的catLevel,

(2)如果待移动节点和目标节点的catLevel不同,则认为是跨级移动。如果是移动到父节点中,则需要设置catLevel,parentID和顺序。此时需要分两种情况来考虑,如果是移动到父节点中,则需要设置catLevel,parentID和顺序,如果是移动到兄弟节点中,则需要设置

包含移动到父节点同级目录,兄弟节点中。

设置菜单拖动开关

 <el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch>

批量删除

  <el-button type="danger" plain size="small" @click="batchDelete">批量删除</el-button>
 //批量删除
    batchDelete() {
      let checkNodes = this.$refs.menuTree.getCheckedNodes();
 
      //  console.log("被选中的节点:",checkNodes);
 
      let catIds = [];
      for (let i = 0; i < checkNodes.length; i++) {
        catIds.push(checkNodes[i].catId);
      }
 
      this.$confirm(`确定要删除?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(catIds, false)
          }).then(({ data }) => {
            this.$message({
              message: "菜单批量删除成功",
              type: "success"
            });
 
            //重新刷新页面
            this.getMeus();
          });
 
 
        })
        .catch(() => {
          //取消删除
        });
    },

8、新增、修改

<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">
   Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">
   Edit
</el-button>
    edit(data) {
      console.log("要修改的数据", data);
      this.dialogType = "edit"
      this.title = "修改分类"
      this.dialogVisible = true

      //发送请求获取当前节点最新的数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: 'get'
      }).then(({ data }) => {
        //请求成功
        console.log("要回显的数据", data);
        this.category.name = data.data.name
        this.category.icon = data.data.icon
        this.category.productUnit = data.data.productUnit
        this.category.catId = data.data.catId
        this.category.parentCid = data.data.parentCid
        this.category.catLevel = data.data.catLevel
        this.category.sort = data.data.sort
        this.category.showStatus = data.data.showStatus
      })
    },

    append(data) {
      console.log("append", data);
      this.dialogType = "add"
      this.title = "添加分类"
      this.dialogVisible = true
      this.category.parentCid = data.catId
      this.category.catLevel = data.catLevel * 1 + 1
      this.category.name = ""
      this.category.icon = ""
      this.category.productUnit = ""
      this.category.catId = null
      this.category.sort = 0
      this.category.showStatus = 1
    },

1.2、品牌管理

1、使用逆向工程生成的前后端代码(P59)

系统管理->菜单管理新增品牌管理

将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件

brand.vue brand-add-or-update.vue

显示的页面没有新增和删除功能,这是因为权限控制的原因

<el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>

查看“isAuth”的定义位置在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。

export function isAuth (key) {
  // return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
  return true
}

再次刷新页面能够看到,按钮就可以出现了。

2、快速显示开关(P60)

brand.vue

        <template slot-scope="scope">
          <el-switch 
            v-model="scope.row.showStatus" 
            active-color="#13ce66" 
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
            @change="updateBrandStatus(scope.row)">
          </el-switch>
        </template>

brand-add-or-update.vue

      <el-form-item label="显示状态" prop="showStatus">
        <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
      </el-form-item>
//更新开关的状态
    updateBrandStatus(data) {
      console.log("最新状态", data);
      let {brandId,showStatus} = data;
      this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        data: this.$http.adornData({brandId,showStatus}, false)
      }).then(({ data }) => {
 
        this.$message({
          message: "状态更新成功",
          type: "success"
        });
 
      });
    },

3、阿里云对象存储-OSS(P61)

谷粒商城项目学习笔记三_第9张图片

和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。

这里我们选择将图片放置到阿里云上,使用对象存储OSS。

对象存储OSS

基本概念

存储空间(Bucket)

对象(Object)

Endpoint(访问域名)

AccessKey(访问密钥)

谷粒商城项目学习笔记三_第10张图片

Java简单上传

Java简单上传

添加依赖在product模块的pom.xml

        <dependency>
            <groupId>com.aliyun.ossgroupId>
            <artifactId>aliyun-sdk-ossartifactId>
            <version>3.10.2version>
        dependency>
    @Test
    public void testUpload(){
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "创建的RAM账号的accessKeyId";
        String accessKeySecret = "创建的RAM账号的accessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "gulimall-fxz";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "7ae0120ec27dc3a7.jpg";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            FileInputStream inputStream = new FileInputStream(
                    "D:\\docs\\pics\\7ae0120ec27dc3a7.jpg");
            ossClient.putObject(bucketName, objectName, inputStream);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
                System.out.println("上传完成...");
            }
        }
    }

使用SpringCloud Alibaba

参考:https://help.aliyun.com/document_detail/108650.html

(1)添加依赖在product模块的pom.xml

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alicloud-ossartifactId>
            <version>2.2.0.RELEASEversion>
        dependency>





(2)创建“AccessKey ID”和“AccessKeySecret”

(3)配置key,secret和endpoint相关信息

  cloud:
    alicloud:
      access-key: 创建的RAM账号的accessKeyId
      secret-key: 创建的RAM账号的accessKeySecret
      oss:
        endpoint: https://oss-cn-beijing.aliyuncs.com

4)注入OSSClient并进行文件的操作

    @Autowired
    OSS ossClient;

    @Test
    public void testUpload(){
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        //String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        //String accessKeyId = "创建的RAM账号的accessKeyId";
        //String accessKeySecret = "创建的RAM账号的accessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "gulimall-fxz";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "7ae0120ec27dc3a7.jpg";

        // 创建OSSClient实例。
        //OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            FileInputStream inputStream = new FileInputStream(
                    "D:\\Projects\\Java培训相关\\尚硅谷\\尚硅谷谷粒商城电商项目\\1.分布式基础(全栈开发篇)\\docs\\pics\\7ae0120ec27dc3a7.jpg");
            ossClient.putObject(bucketName, objectName, inputStream);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
                System.out.println("上传完成...");
            }
        }
    }

4、阿里云对象存储-服务端签名后直传(P63)

新建gulimall-third-party

建模块过程中遇到编译错误

1、确保springboot和springcloud版本一致

        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.8.RELEASEversion>

	<properties>
        <java.version>1.8java.version>
        <spring-cloud.version>Greenwich.SR3spring-cloud.version>
    properties>

				<groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>

2、排除datasource

@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
public class GulimallThirdPartyApplication {

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

}

3、测试类和方法都是public

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

    @Test
    public void contextLoads() {
    }
}

背景

服务端签名后直传 - 对象存储 OSS - 阿里云

采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。

原理介绍

img

服务端签名后直传的原理如下:

  1. 用户发送上传Policy请求到应用服务器。
  2. 应用服务器返回上传Policy和签名给用户。
  3. 用户直接上传数据到OSS。

编写“com.atguigu.gulimall.thridparty.controller.OSSController”类:

/**
 * ClassName: OssController
 * Package: com.atguigu.gulimall.thirdparty.controller
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/4/20 - 12:30
 * @Version: v1.0
 */

@RestController
public class OssController {

    @Autowired
    OSS ossClient;


    @RequestMapping("/oss/policy")
    public R policy(){
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessId = "你的accessId";
        String accessKey = "你的accessKey";
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-beijing.aliyuncs.com";
        // 填写Bucket名称,例如examplebucket。
        String bucket = "gulimall-fxz";
        // 填写Host地址,格式为https://bucketname.endpoint。
        String host = "https://"+bucket+"."+endpoint;
        // 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
//        String callbackUrl = "https://192.168.0.0:8888";
        // 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
        String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        String dir = format+"/";

        // 创建ossClient实例。
        // OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
        Map<String, String> respMap = null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            respMap = new LinkedHashMap<String, String>();
            respMap.put("accessId", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
            // respMap.put("expire", formatISO8601Date(expiration));


        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        }

        return R.ok().put("data", respMap);
    }

}

以后在上传文件时的访问路径为“ http://localhost:88/api/thirdparty/oss/policy”,

在“gulimall-gateway”中配置路由规则:

        - id: third_part_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
            - RewritePath=/api/thirdparty/(?>/?.*),/$\{segment}

上传组件

放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传。

前端上传使用ElementUI组件的Upload 上传,文件选择后后触发before-upload钩子函数

      beforeUpload(file) {
        let _self = this;
        return new Promise((resolve, reject) => {
          policy().then(response => { //获取服务端返回的policy和签名
            console.log("响应的数据",response);
            _self.dataObj.policy = response.data.policy;
            _self.dataObj.signature = response.data.signature;
            _self.dataObj.ossaccessKeyId = response.data.accessId;
            _self.dataObj.key = response.data.dir + getUUID()+'_${filename}';
            _self.dataObj.dir = response.data.dir;
            _self.dataObj.host = response.data.host;
            resolve(true)
          }).catch(err => {
            reject(false)
          })
        })
      },

policy.js

import http from '@/utils/httpRequest.js'
export function policy() {
   return  new Promise((resolve,reject)=>{//发送获取服务端签名的请求
        http({
            url: http.adornUrl("/thirdparty/oss/policy"),
            method: "get",
            params: http.adornParams({})
        }).then(({ data }) => {
            resolve(data);
        })
    });
}

5、JSR303校验(P66)

步骤1:使用校验注解

在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。

在非空处理方式上提供了@NotNull,@Blank和@

(1)@NotNull

The annotated element must not be null. Accepts any type.
注解元素禁止为null,能够接收任何类型

(2)@NotEmpty

the annotated element must not be null nor empty.

该注解修饰的字段不能为null或""

Supported types are:

支持以下几种类型

CharSequence (length of character sequence is evaluated)

字符序列(字符序列长度的计算)

Collection (collection size is evaluated)
集合长度的计算

Map (map size is evaluated)
map长度的计算

Array (array length is evaluated)
数组长度的计算

(3)@NotBlank

The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
该注解不能为null,并且至少包含一个非空白字符。接收字符序列。

步骤2:在请求方法中,使用校验注解@Valid,开启校验

    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand){
		brandService.save(brand);
 
        return R.ok();
    }

测试: http://localhost:88/api/product/brand/save

在postman种发送上面的请求

{
    "timestamp": "2023-04-21T09:20:46.383+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.brandEntity.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "brandEntity.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "不能为空",
            "objectName": "brandEntity",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='brandEntity'. Error count: 1",
    "path": "/product/brand/save"
}

能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:

javax.validation.constraints.AssertFalse.message     = 只能为false
javax.validation.constraints.AssertTrue.message      = 只能为true
javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}
javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}
javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址
javax.validation.constraints.Future.message          = 需要是一个将来的时间
javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
javax.validation.constraints.Max.message             = 最大不能超过{value}
javax.validation.constraints.Min.message             = 最小不能小于{value}
javax.validation.constraints.Negative.message        = 必须是负数
javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零
javax.validation.constraints.NotBlank.message        = 不能为空
javax.validation.constraints.NotEmpty.message        = 不能为空
javax.validation.constraints.NotNull.message         = 不能为null
javax.validation.constraints.Null.message            = 必须为null
javax.validation.constraints.Past.message            = 需要是一个过去的时间
javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间
javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"
javax.validation.constraints.Positive.message        = 必须是正数
javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零
javax.validation.constraints.Size.message            = 个数必须在{min}{max}之间

org.hibernate.validator.constraints.CreditCardNumber.message        = 不合法的信用卡号码
org.hibernate.validator.constraints.Currency.message                = 不合法的货币 (必须是{value}其中之一)
org.hibernate.validator.constraints.EAN.message                     = 不合法的{type}条形码
org.hibernate.validator.constraints.Email.message                   = 不是一个合法的电子邮件地址
org.hibernate.validator.constraints.Length.message                  = 长度需要在{min}{max}之间
org.hibernate.validator.constraints.CodePointLength.message         = 长度需要在{min}{max}之间
org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}的校验码不合法, Luhn10校验和不匹配
org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}的校验码不合法,10校验和不匹配
org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}的校验码不合法,11校验和不匹配
org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
org.hibernate.validator.constraints.NotBlank.message                = 不能为空
org.hibernate.validator.constraints.NotEmpty.message                = 不能为空
org.hibernate.validator.constraints.ParametersScriptAssert.message  = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.Range.message                   = 需要在{min}{max}之间
org.hibernate.validator.constraints.SafeHtml.message                = 可能有不安全的HTML内容
org.hibernate.validator.constraints.ScriptAssert.message            = 执行脚本表达式"{script}"没有返回期望结果
org.hibernate.validator.constraints.URL.message                     = 需要是一个合法的URL

org.hibernate.validator.constraints.time.DurationMax.message        = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
org.hibernate.validator.constraints.time.DurationMin.message        = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}

想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是

public @interface NotBlank {
 
	String message() default "{javax.validation.constraints.NotBlank.message}";

可以在添加注解的时候,修改message:

	@NotBlank(message = "品牌名必须非空")
	private String name;

当再次发送请求时,得到的错误提示信息:

{
    "timestamp": "2023-04-21T09:36:04.125+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotBlank.brandEntity.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "brandEntity.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "品牌名必须非空",
            "objectName": "brandEntity",
            "field": "name",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "message": "Validation failed for object='brandEntity'. Error count: 1",
    "path": "/product/brand/save"
}

但是这种返回的错误结果并不符合我们的业务需要。

步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。

 @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
        if( result.hasErrors()){
            Map<String,String> map=new HashMap<>();
            //1.获取错误的校验结果
            result.getFieldErrors().forEach((item)->{
                //获取发生错误时的message
                String message = item.getDefaultMessage();
                //获取发生错误的字段
                String field = item.getField();
                map.put(field,message);
            });
            return R.error(400,"提交的数据不合法").put("data",map);
        }else {
 
        }
		brandService.save(brand);
 
        return R.ok();
    }
 

这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。

步骤4:统一异常处理

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。

(1)抽取一个异常处理类

/**
 * ClassName: GulimallExceptionControllerAdvice
 * Package: com.atguigu.gulimall.product.exception
 * Description: 集中处理所有异常
 *
 * @Author: Mr-Feng
 * @Create: 2023/4/26 - 13:20
 * @Version: v1.0
 */
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e){
        log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
        BindingResult bindingResult = e.getBindingResult();

        Map<String,String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),
                BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),
                BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
}

(2)测试: http://localhost:88/api/product/brand/save

image-20200429183334783

(3)默认异常处理

   @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){
        log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
        return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg());
    }

(4)错误状态码

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

@ControllerAdvice+@ExceptionHandler
系统错误码

/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为 5 为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知
异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*
*
*/

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

/**
 * ClassName: BizCodeEnume
 * Package: com.atguigu.common.exception
 * Description:
 * 错误码和错误信息定义类
 *  * 1. 错误码定义规则为5为数字
 *  * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 *  * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 *  * 错误码列表:
 *  *  10: 通用
 *  *      001:参数格式校验
 *  *  11: 商品
 *  *  12: 订单
 *  *  13: 购物车
 *  *  14: 物流
 * @Author: Mr-Feng
 * @Create: 2023/5/15 - 17:01
 * @Version: v1.0
 */
public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

(5)测试: http://localhost:88/api/product/brand/save

image-20200429191830967

分组校验功能(完成多场景的复杂校验)

1、给校验注解,标注上groups,指定什么情况下才需要进行校验

如:指定在更新和添加的时候,都需要进行校验

	@NotEmpty
	@NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class})
	private String name;

在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

2、业务方法参数上使用@Validated注解

@Validated的value方法:

Specify one or more validation groups to apply to the validation step kicked off by this annotation.
指定一个或多个验证组以应用于此注释启动的验证步骤。

JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using
them as type-safe group arguments, as implemented in SpringValidatorAdapter.

JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。

Other SmartValidator implementations may support class arguments in other ways as well.

其他SmartValidator 实现也可以以其他方式支持类参数。

3、默认情况下,在分组校验情况下,没有指定分组的校验注解,将不会生效,它只会在分组的情况下生效。

自定义校验功能

1、编写一个自定义的校验注解

/**
 * ClassName: ListValue
 * Package: com.atguigu.common.valid
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/5/15 - 20:02
 * @Version: v1.0
 */

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "{com.atguigu.common.valid.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    int[] vals() default {};
}

2、编写一个自定义的校验器

/**
 * ClassName: ListValueConstraintValidator
 * Package: com.atguigu.common.valid
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/5/15 - 20:18
 * @Version: v1.0
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set = new HashSet<>();
    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    //判断是否校验成功

    /**
     *
     * @param value 需要校验的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

3、关联自定义的校验器和自定义的校验注解

@Constraint(validatedBy = { ListValueConstraintValidator.class})

4、使用实例

	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@ListValue(value = {0,1},groups ={AddGroup.class})
	private Integer showStatus;

1.3、属性分组

1、SPU-SKU-属性(P70)

SPU:Standard Product Unit(标准化产品单元)
是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一
个产品的特性。
iphoneX 是 SPU、MI 8 是 SPU
iphoneX 64G 黑曜石 是 SKU
MI8 8+64G+黑色 是 SKU
SKU:Stock Keeping Unit(库存量单位)
即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU 这是对于大型连锁超市
DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每
种产品均对应有唯一的 SKU 号。

2、【属性分组-规格参数-销售属性-三级分类】关联关系

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;

  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组
  • 属性的分组也是以三级分类组织起来的
  • 属性名确定的,但是值是每一个商品不同来决定的

谷粒商城项目学习笔记三_第11张图片

谷粒商城项目学习笔记三_第12张图片

3、属性分组-效果

谷粒商城项目学习笔记三_第13张图片

1.4、平台属性

1、规格参数

1)、保存属性【规格参数,销售属性】

POST    /product/attr/save

请求参数

{
  "attrGroupId": 0, //属性分组id
  "attrName": "string",//属性名
  "attrType": 0, //属性类型
  "catelogId": 0, //分类id
  "enable": 0, //是否可用 
  "icon": "string", //图标
  "searchType": 0, //是否检索
  "showDesc": 0, //快速展示
  "valueSelect": "string", //可选值列表
  "valueType": 0 //可选值模式
}

分页数据

响应数据

{
    "msg": "success",
    "code": 0
}

规格参数新增时,请求的URL:Request URL:

http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key=

当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范。

1588732021702

比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。在java中,涉及到了这几种类型。

1588732152646

Request URL: http://localhost:88/api/product/attr/save,现在的情况是,它在保存的时候,只是保存了attr,并没有保存attrgroup,为了解决这个问题,我们新建了一个vo/AttrVo,在原AttrEntity基础上增加了attrGroupId字段,使得保存新增数据的时候,也保存了它们之间的关系。

通过" BeanUtils.copyProperties(attr,attrEntity);"能够实现在两个Bean之间拷贝数据,但是两个Bean的字段要相同

修改“com.atguigu.gulimall.product.controller.AttrController”类,代码如下:

 /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody AttrVo attr){
		attrService.saveAttr(attr);
 
        return R.ok();
    }

修改“com.atguigu.gulimall.product.service.AttrService”类,代码如下:

void saveAttr(AttrVo attr);

修改“com.atguigu.gulimall.product.service.impl.AttrServiceImpl”类,代码如下:

 @Transactional
    @Override
    public void saveAttr(AttrVo attr) {
        AttrEntity attrEntity = new AttrEntity();
        BeanUtils.copyProperties(attr,attrEntity);//Spring提供的工具类,用于复制对象属性,使用这个工具的前提条件是属性的字段名必须一致。
        //1、保存基本数据
        this.save(attrEntity);
        //2、保存关联关系
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        relationEntity.setAttrGroupId(attr.getAttrGroupId());
        relationEntity.setAttrId(attrEntity.getAttrId());
        attrAttrgroupRelationDao.insert(relationEntity);
    }

2)、规格参数列表(P77)

GET		/product/attr/base/list/{catelogId}

请求参数

{
   page: 1,//当前页码
   limit: 10,//每页记录数
   sidx: 'id',//排序字段
   order: 'asc/desc',//排序方式
   key: '华为'//检索关键字
}

分页数据

响应数据

{
	"msg": "success",
	"code": 0,
	"page": {
		"totalCount": 0,
		"pageSize": 10,
		"totalPage": 0,
		"currPage": 1,
		"list": [{
			"attrId": 0, //属性id
			"attrName": "string", //属性名
			"attrType": 0, //属性类型,0-销售属性,1-基本属性
			"catelogName": "手机/数码/手机", //所属分类名字
			"groupName": "主体", //所属分组名字
			"enable": 0, //是否启用
			"icon": "string", //图标
			"searchType": 0,//是否需要检索[0-不需要,1-需要]
			"showDesc": 0,//是否展示在介绍上;0-否 1-是
			"valueSelect": "string",//可选值列表[用逗号分隔]
			"valueType": 0//值类型[0-为单个值,1-可以选择多个值]
		}]
	}
}

响应数据不仅包括AttrVo基本数据,还多包含了2个字段,因此新建AttrRespVo,用于规格参数列表显示。

/**
 * ClassName: AttrRespVo
 * Package: com.atguigu.gulimall.product.vo
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/5/24 - 10:02
 * @Version: v1.0
 */
@Data
public class AttrRespVo extends AttrVo{
    /*
    * "catelogName": "手机/数码/手机", //所属分类名字
	* "groupName": "主体", //所属分组名字
    * */
    private String catelogName;
    private String groupName;
}

修改“com.atguigu.gulimall.product.controller.AttrController”类,代码如下:

    // /product/attr/base/list/{catelogId}
    @GetMapping("/base/list/{catelogId}")
    public R baseList(@RequestParam Map<String, Object> params, @PathVariable("catelogId")Long catelogId){
        PageUtils page = attrService.queryBaseAttrPage(params, catelogId);
        return R.ok().put("page", page);
    }

修改“com.atguigu.gulimall.product.service.AttrService”类,代码如下:

    PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId)

修改“com.atguigu.gulimall.product.service.impl.AttrServiceImpl”类,代码如下:

    @Override
    public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId) {
        QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();

        if (catelogId != 0) {
            wrapper.eq("catelogId",catelogId);
        }

        String key = (String) params.get("key");
        if(StringUtils.isEmpty(key)){
            //attr_id attr_name
            wrapper.and((obj)->{
                obj.eq("attr_id", key).or().like("attr_name",key);
            });
        }

        IPage<AttrEntity> page = this.page(
                new Query<AttrEntity>().getPage(params),
                wrapper
        );

        PageUtils pageUtils = new PageUtils(page);
        List<AttrEntity> records = page.getRecords();
        List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
            AttrRespVo attrRespVo = new AttrRespVo();
            BeanUtils.copyProperties(attrEntity, attrRespVo);
            //1、设置分类和分组的名字
            AttrAttrgroupRelationEntity attrid = relationDao.selectOne(
                    new QueryWrapper<AttrAttrgroupRelationEntity>()
                            .eq("attr_id", attrEntity.getAttrId()));
            if (attrid != null) {
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrid.getAttrGroupId());
                attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
            }
            CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
            if (categoryEntity != null) {
                attrRespVo.setCatelogName(categoryEntity.getName());
            }

            return attrRespVo;
        }).collect(Collectors.toList());
        pageUtils.setList(respVos);
        return pageUtils;
    }

1.5、发布商品

1、加入前端其他模块

商品管理需要会员等级,先把资料前端文件夹里的其他modules:member、order、ware、coupon里的文件导入vsCode里重新运行,添加几个会员。

2、pubsub安装(P83)

在商品发布章节,如果遇到提示 ”PubSub “未定义错误,则需要安装 pubsub-js,具体步骤:
(1)安装 pubsub-js:

npm install --save pubsub-js --legacy-peer-deps

(2)在 main.js 中引入

//导入
import PubSub from 'pubsub-js'
//挂载全局
Vue.prototype.PubSub = PubSub

3、新增商品(P83-P94)

POST   /product/spuinfo/save

请求参数

{
	"spuName": "Apple XR",
	"spuDescription": "Apple XR",
	"catalogId": 225,
	"brandId": 12,
	"weight": 0.048,
	"publishStatus": 0,
	"decript": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//66d30b3f-e02f-48b1-8574-e18fdf454a32_f205d9c99a2b4b01.jpg"],
	"images": ["https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg", "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg"],
	"bounds": {
		"buyBounds": 500,
		"growBounds": 6000
	},
	"baseAttrs": [{
		"attrId": 7,
		"attrValues": "aaa;bb",
		"showDesc": 1
	}, {
		"attrId": 8,
		"attrValues": "2019",
		"showDesc": 0
	}],
	"skus": [{
		"attr": [{
			"attrId": 9,
			"attrName": "颜色",
			"attrValue": "黑色"
		}, {
			"attrId": 10,
			"attrName": "内存",
			"attrValue": "6GB"
		}],
		"skuName": "Apple XR 黑色 6GB",
		"price": "1999",
		"skuTitle": "Apple XR 黑色 6GB",
		"skuSubtitle": "Apple XR 黑色 6GB",
		"images": [{
			"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//dcfcaec3-06d8-459b-8759-dbefc247845e_5b5e74d0978360a1.jpg",
			"defaultImg": 1
			}, {
			"imgUrl": "https://gulimall-hello.oss-cn-beijing.aliyuncs.com/2019-11-22//5b15e90a-a161-44ff-8e1c-9e2e09929803_749d8efdff062fb0.jpg",
			"defaultImg": 0
		}],
		"descar": ["黑色", "6GB"],
		"fullCount": 5,
		"discount": 0.98,
		"countStatus": 1,
		"fullPrice": 1000,
		"reducePrice": 10,
		"priceStatus": 0,
		"memberPrice": [{
			"id": 1,
			"name": "aaa",
			"price": 1998.99
		}]
		}, {
		"attr": [{
			"attrId": 9,
			"attrName": "颜色",
			"attrValue": "黑色"
		}, {
			"attrId": 10,
			"attrName": "内存",
			"attrValue": "12GB"
		}],
		"skuName": "Apple XR 黑色 12GB",
		"price": "2999",
		"skuTitle": "Apple XR 黑色 12GB",
		"skuSubtitle": "Apple XR 黑色 6GB",
		"images": [{
			"imgUrl": "",
			"defaultImg": 0
		}, {
			"imgUrl": "",
			"defaultImg": 0
		}],
		"descar": ["黑色", "12GB"],
		"fullCount": 0,
		"discount": 0,
		"countStatus": 0,
		"fullPrice": 0,
		"reducePrice": 0,
		"priceStatus": 0,
		"memberPrice": [{
			"id": 1,
			"name": "aaa",
			"price": 1998.99
		}]
	}, {
		"attr": [{
			"attrId": 9,
			"attrName": "颜色",
			"attrValue": "白色"
		}, {
			"attrId": 10,
			"attrName": "内存",
			"attrValue": "6GB"
		}],
		"skuName": "Apple XR 白色 6GB",
		"price": "1998",
		"skuTitle": "Apple XR 白色 6GB",
		"skuSubtitle": "Apple XR 黑色 6GB",
		"images": [{
			"imgUrl": "",
			"defaultImg": 0
		}, {
			"imgUrl": "",
			"defaultImg": 0
		}],
		"descar": ["白色", "6GB"],
		"fullCount": 0,
		"discount": 0,
		"countStatus": 0,
		"fullPrice": 0,
		"reducePrice": 0,
		"priceStatus": 0,
		"memberPrice": [{
			"id": 1,
			"name": "aaa",
			"price": 1998.99
		}]
	}, {
		"attr": [{
			"attrId": 9,
			"attrName": "颜色",
			"attrValue": "白色"
		}, {
			"attrId": 10,
			"attrName": "内存",
			"attrValue": "12GB"
		}],
		"skuName": "Apple XR 白色 12GB",
		"price": "2998",
		"skuTitle": "Apple XR 白色 12GB",
		"skuSubtitle": "Apple XR 黑色 6GB",
		"images": [{
			"imgUrl": "",
			"defaultImg": 0
		}, {
			"imgUrl": "",
			"defaultImg": 0
		}],
		"descar": ["白色", "12GB"],
		"fullCount": 0,
		"discount": 0,
		"countStatus": 0,
		"fullPrice": 0,
		"reducePrice": 0,
		"priceStatus": 0,
		"memberPrice": [{
			"id": 1,
			"name": "aaa",
			"price": 1998.99
		}]
	}]
}

分页数据

响应数据

{
	"msg": "success",
	"code": 0
}

新增VO

通过工具将JSON字符串转换为Java实体类,将请求参数转换为VO。包括:SpuSaveVo、Skus、MemberPrice、Images、Bounds、BaseAttrs、Attr。

SpuSaveVo.java

@Data
public class SpuSaveVo {

    private String spuName;
    private String spuDescription;
    private Long catalogId;
    private Long brandId;
    private BigDecimal weight;
    private int publishStatus;
    private List<String> decript;
    private List<String> images;
    private Bounds bounds;
    private List<BaseAttrs> baseAttrs;
    private List<Skus> skus;



}

修改“com.atguigu.gulimall.product.controller.SpuInfoController”类,代码如下:

     /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody SpuSaveVo vo){
		//spuInfoService.save(spuInfo);
        spuInfoService.save(vo);
 
        return R.ok();
    }

修改“com.atguigu.gulimall.product.service.SpuInfoService”类,代码如下:

void save(SpuSaveVo vo);
 
void saveBaseSpuInfo(SpuInfoEntity infoEntity);

修改“com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl”类,代码如下:


//5、保存spu的积分信息 gulimall_sms --> sms_spu_bounds

需要远程调用第三方服务

注意重点: \textcolor{red} {注意重点:} 注意重点:

1、创建openFeign配置(前提第三方服务已经注册和配置到注册中心了)

/**
 * ClassName: CouponFeignService
 * Package: com.atguigu.gulimall.product.feign
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/6/1 - 10:09
 * @Version: v1.0
 */
@FeignClient("gulimall-coupon")//1、声明调用那个远程服务
public interface CouponFeignService {
}

2、在主程序类中加上@EnableFeignClients(basePackages = “com.atguigu.gulimall.product.feign”)

@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")//2、开启远程调用功能
@MapperScan("com.atguigu.gulimall.product.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {

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

}

3、在gulimall-common添加服务与服务之间调用的to类“com.atguigu.common.to.SpuBoundTo”类,代码如下:

@Data
public class SpuBoundTo {
    private Long SpuId;
    private BigDecimal buyBounds;
    private BigDecimal growBounds;
}

修改“com.atguigu.gulimall.product.feign.CouponFeignService”类,代码如下:

/**
 * ClassName: CouponFeignService
 * Package: com.atguigu.gulimall.product.feign
 * Description:
 *
 * @Author: Mr-Feng
 * @Create: 2023/6/1 - 10:09
 * @Version: v1.0
 */
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    /**
     * 1、couponFeignService.saveSpuBounds(spuBoundTo)
     *      1)、@RequestBody将这个对象转为json
     *      2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。将上一步转的json放在请求体的位置发送请求
     *      3)、对方服务收到请求请求体有json数据。
     *      (@RequestBody SpuBoundsEntity spuBounds)将请求体里的json转为SpuBoundsEntity
     * 只要json数据模型是兼容的。对方服务无需使用同一个to
     * @param spuBoundTo
     * @return
     */
    @PostMapping("/coupon/spubounds/save")
    R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);

4、服务接口

    /**
     * 保存
     */
    @PostMapping("/save")
    //@RequiresPermissions("coupon:spubounds:save")
    public R save(@RequestBody SpuBoundsEntity spuBounds){
		spuBoundsService.save(spuBounds);

        return R.ok();
    }

设置批量重启和内存占用

谷粒商城项目学习笔记三_第14张图片
谷粒商城项目学习笔记三_第15张图片

测试

设置数据库隔离级别为未提交可读,方便调试

SET SESSION TRANSACTION ISOLATION LEVEL	READ UNCOMMITTED;

修改主键为手动输入非自动

@TableId(type = IdType.INPUT)//指定该主键不是自增的,是我们手动输入的

/**
 * spu信息介绍
 * 
 * @author xinzhouf
 * @email [email protected]
 * @date 2023-03-24 09:26:12
 */
@Data
@TableName("pms_spu_info_desc")
public class SpuInfoDescEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 商品id
	 */
	@TableId(type = IdType.INPUT)//指定该主键不是自增的,是我们手动输入的
	private Long spuId;
	/**
	 * 商品介绍
	 */
	private String decript;

}

1.6、仓库管理(P95)

1、整合ware服务

gateway网关模块的application.yml添加ware路由

        - id: ware_route
          uri: lb://gulimall-ware
          predicates:
            - Path=/api/ware/**
          filters:
            - RewritePath=/api/(?>/?.*),/$\{segment}

GulimallWareApplication.java中添加Mapper扫描和事务管理

@EnableTransactionManagement
@MapperScan("com.atguigu.gulimall.ware.dao")

2、采购简要流程(P97)

谷粒商城项目学习笔记三_第16张图片

3、远程查询sku的名字

创建feign.ProductFeignService的接口

/**
 * ClassName: ProductFeignService
 * Package: com.atguigu.gulimall.ware.feign
 * Description:
 *   1)、让所有请求过网关;
 *          1、@FeignClient("gulimall-gateway"):给gulimall-gateway所在的机器发请求
 *          2、/api/product/skuinfo/info/{skuId}
 *   2)、直接让后台指定服务处理
 *          1、@FeignClient("gulimall-product")
 *          2、/product/skuinfo/info/{skuId}
 *
 * @Author: Mr-Feng
 * @Create: 2023/6/2 - 14:40
 * @Version: v1.0
 */

@FeignClient("gulimall-product") //调用远程服务的模块
public interface ProductFeignService {
    @RequestMapping("/product/skuinfo/info/{skuId}")
    public R info(@PathVariable("skuId") Long skuId);
}

GulimallWareApplication.java中开启服务

@EnableFeignClients	//开启远程Feign服务
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {

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

}

修改WareSkuServiceImpl.java

	...
    @Autowired
    ProductFeignService productFeignService;
	...
	...
            //TODO 远程查询sku的名字,如果失败,整个事务无需回滚
            //1、自己catch异常
            //TODO 还可以用什么办法让异常出现以后不回滚?高级
            try {
                R info = productFeignService.info(skuId);
                Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");

                if(info.getCode() == 0){
                    skuEntity.setSkuName((String) data.get("skuName"));
                }
            }catch (Exception e){

            }
	...

4、点击规格找不到页面(P100)

解决:

1、在数据库gulimall_admin执行以下sql再刷新页面:

INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);

2、在 /src/router/index.js 在mainRoutes->children里面加上:

{ path: '/product-attrupdate', component: _import('modules/product/attrupdate'), name: 'attr-update', meta: { title: '规格维护', isTab: true } }

你可能感兴趣的:(谷粒商城,学习,笔记,java)