商品服务-API-属性分组

1. 品牌管理基本概念

1.1 SPU和SKU

SPU:Standard Product Unit(标准化产品单元)

  • 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

SKU:Stock Keeping Unit(库存量单位)

  • 即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。
  • SKU 这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。

例:

iphoneX 是SPU、MI8 是SPU
iphoneX 64G 黑曜石是SKU
MI8 8+64G 黑色是SKU

1.2 基本属性【规格参数】与销售属性

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

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

决定SKU的属性称为销售属性。
决定SPU的属性称为基本属性【规格参数】。

2. 属性分组展示功能

2.1 前端页面效果

复制逆向代码生成的attrgroup.vueattrgroup-add-or-update到product目录下。

  • <template>标签体内将页面分割为6栅格和18栅格两部分。
  • 6栅格部分展示提取出来的公共三级分类category.vue
<el-col :span="6">
  <category></category>
</el-col>
import Category from "../common/category.vue";
  • 18栅格部分展示三级分类表格。(将逆向生成的前端直接复制即可。)
<el-col :span="18">
  <div class="mod-config">
    <el-form :inline="true" :model="dataForm" @keyup.enter.native="getDataList()">
      <el-form-item>
        <el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
      </el-form-item>
      <el-form-item>
        <el-button @click="getDataList()">查询</el-button>
        <el-button v-if="isAuth('product:attrgroup:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
        <el-button v-if="isAuth('product:attrgroup:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
      </el-form-item>
    </el-form>
    <el-table :data="dataList" border v-loading="dataListLoading" @selection-change="selectionChangeHandle" style="width: 100%;">
      <el-table-column type="selection" header-align="center" align="center" width="50">
      </el-table-column>
      <el-table-column prop="attrGroupId" header-align="center" align="center" label="分组id">
      </el-table-column>
      <el-table-column prop="attrGroupName" header-align="center" align="center" label="组名">
      </el-table-column>
      <el-table-column prop="sort" header-align="center" align="center" label="排序">
      </el-table-column>
      <el-table-column prop="descript" header-align="center" align="center" label="描述">
      </el-table-column>
      <el-table-column prop="icon" header-align="center" align="center" label="组图标">
      </el-table-column>
      <el-table-column prop="catelogId" header-align="center" align="center" label="所属分类id">
      </el-table-column>
      <el-table-column fixed="right" header-align="center" align="center" width="150" label="操作">
        <template slot-scope="scope">
          <el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button>
          <el-button type="text" size="small" @click="deleteHandle(scope.row.attrGroupId)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" :current-page="pageIndex" :page-sizes="[10, 20, 50, 100]" :page-size="pageSize" :total="totalPage"
      layout="total, sizes, prev, pager, next, jumper">
    </el-pagination>
    <!-- 弹窗, 新增 / 修改 -->
    <add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
  </div>
</el-col>

整体效果

2.2 父子组件传递数据

要求: 点击左侧栅格的三级分类,右边表格自动展示该分类的属性。

2.2.1 子组件获取点击数据

  • 给组件绑定一个nodeclick事件,当该树形结构变点击后,会回调返回三个参数。
  • 将这个事件绑定我们自定义的方法,就可以获取这三个参数值。
 <el-tree :data="menus" :props="defaultProps" node-key="catId" ref="menuTree" @node-click="nodeclick"></el-tree>
methods: {
  getMenus() {
    this.$http({
      url: this.$http.adornUrl("/product/category/list/tree"),
      method: "get",
    }).then(({ data }) => {
      // 将后端请求获取的data传递给menus。
      this.menus = data.page;
    });
  },
  nodeclick(data,node,component){
    console.log("树节点被点击了",data,node,component)
  }
},
  • 当点击一个树形数据时,可以在子组件获取到点击的数据。

2.2.2 通过事件将数据传给父组件

  • 通过 this.$emit()和父组件进行绑定,并传递数据。

子组件

nodeclick(data,node,component){
  console.log("树节点被点击了",data,node,component);
  //向父组件发送事件(携带参数)和父组件的一个方法进行绑定。
  this.$emit("tree-node-click",data,node,component)
}

父组件

  • 收到子组件散发的事件,和自身组件的一个方法进行绑定。
<category @tree-node-click="treenodeclick"></category>

treenodeclick(data, node, component) {
    console.log("父组件收到数据", data);
  },

2.2 后端获取数据

前端获取点击后的节点数据,并且能够传递给父组件,父组件可以根据该id向后端发请求,获取该id下的所有属性数据。

  • 根据接口文档编写代码获取数据

Query和QueryWapper用法

2.2.1 Controller层

@RequestMapping("/list/{catelogId}")
public R list(@RequestParam Map<String, Object> params,
              @PathVariable("catelogId") Long catelogId){
    //根据catelogId查询,返回分页数据
    PageUtils page = attrGroupService.queryPage(params,catelogId);
    return R.ok().put("page", page);
}

2.2.2 Service层

前端传递的params:

  • 当前页,每页显示几条,检索字。
 params: this.$http.adornParams({
  page: this.pageIndex,
  limit: this.pageSize,
  key: this.dataForm.key,
}),

service层根据前端传递的catId判断查询数据,将数据封装为page对象返回。

  • catId为0(默认查所有数据)
  • catId不为0(查符合条件的数据)
  • key不为空(根据key进行sql拼接)
@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
    String key = (String) params.get("key");
    if (catelogId == 0 && StringUtils.isEmpty(key)) {
        //查询所有,第一个参数是IPage(分页数据,包括当前页和每页显示数)
        //第二个参数是查询条件(即sql),没有条件表示查询所有该泛型的数据。
        IPage<AttrGroupEntity> page = this.page(
                new Query<AttrGroupEntity>().getPage(params),
                new QueryWrapper<AttrGroupEntity>()
        );
        //使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
        return new PageUtils(page);
    } else if (catelogId == 0 && !StringUtils.isEmpty(key)) {
        //查询所有,且有关键字时
        //select * from pms_attr_group where  (attr_group_id=key or attr_group_name like %key%)
        QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
        wrapper.and((obj) -> {
            obj.eq("catelog_id", key).or().like("attr_group_name", key);
        });
        IPage<AttrGroupEntity> page = this.page(
                new Query<AttrGroupEntity>().getPage(params), wrapper
        );
        //使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
        return new PageUtils(page);
    } else {
        //根据catelogId和检索关键字查
        //select * from pms_attr_group where catelog_id = catelogId and (attr_group_id=key or attr_group_name like %key%)
        QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
        if (!StringUtils.isEmpty(key)) {
            //有检索条件
            wrapper.and((obj) -> {
                obj.eq("catelog_id", key).or().like("attr_group_name", key);
            });
        }
        IPage<AttrGroupEntity> page = this.page(
                new Query<AttrGroupEntity>().getPage(params), wrapper
        );
        //使用工具类解析IPage对象,获取分页信息(当前页,每页数,总页数,总数据...)
        return new PageUtils(page);
    }
}

2.3 前后端联调测试

2.3.1 前端

//接收子组件点击后的节点数据,将其和方法绑定
<category @tree-node-click="treenodeclick"></category>

//如果是三级节点,给catId赋值,重新发请求查询
treenodeclick(data, node, component) {
  if (node.level == 3) {
    this.catId = data.catId;
    this.getDataList(); //重新获取列表
  }
},

//向后端发请求获取page数据,catId默认是0
getDataList() {
  this.dataListLoading = true;
  this.$http({
    url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),
    method: "get",
    params: this.$http.adornParams({
      page: this.pageIndex,
      limit: this.pageSize,
      key: this.dataForm.key,
    }),
  }).then(({ data }) => {
    if (data && data.code === 0) {
      this.dataList = data.page.list;
      this.totalPage = data.page.totalCount;
    } else {
      this.dataList = [];
      this.totalPage = 0;
    }
    this.dataListLoading = false;
  });
},

2.3.2测试

请求体

携带key时发送请求

3. 属性分组新增功能

  • prop绑定的是选中的属性值。
  • 只需为 Cascader 的options属性指定选项数组即可渲染出一个级联选择器。
  • options 绑定全部的三级分类数据
<el-form-item label="所属分类id" prop="catelogId">
  <el-cascader v-model="dataForm.catelogIds" :options="categorys" :props="props"></el-cascader>
</el-form-item>


data() {
  return {
    categorys: [],
  }
}

methods: {
  getCategorys() {
    this.$http({
      url: this.$http.adornUrl("/product/category/list/tree"),
      method: "get",
    }).then(({ data }) => {
      // 将后端请求获取的data传递给menus。
      this.categorys = data.page;
    });
  },
},
//组件创建的时候就调用方法获取全部的三级分类
created(){
  this.getCategorys();
}
  • 指定显示的值,提交的值,和下级的值。
props:{
  value:"catId",
  label:"name",
  children:"children"
},

出现一个问题,查询出来的三级分类还有一个空的chidredn

解决方法

  • 给返回实体的children属性添加一个注解
// 用来存放该分类的子分类。不存在数据库表,需要加注解标注
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist =false)
private List<CategoryEntity> children;

3.1 点击修改回显选择

其他属性回显正常,catlogId需要是一个层级结构,如:[2,25,225]才回显正常。

3.1.1 后端修改返回值

controller层

  • controller层需要修改一下返回信息,添加一个完整路径属性

AttrGroupController

@RequestMapping("/info/{attrGroupId}")
 // @RequiresPermissions("product:attrgroup:info")
  public R info(@PathVariable("attrGroupId") Long attrGroupId){
		AttrGroupEntity attrGroup = attrGroupService.getById(attrGroupId);
      Long catelogId = attrGroup.getCatelogId();
      //根据属性分组id查询分类id(225),根据分类id查询完整路径[2,25,225]
      Long[] paths=categoryService.findPath(catelogId);
      //将完整路径赋值给新的返回实体
      attrGroup.setCatelogPath(paths);
      return R.ok().put("data", attrGroup);
  }

service层

CategoryService

  • 根据传入的分类id查询完整路径,然后对数组进行反转即可
@Override
public Long[] findPath(Long catelogId) {
    ArrayList<Long> list = new ArrayList<>();
    ArrayList<Long> pList = this.getPath(list, catelogId);
    Collections.reverse(pList);
    //数组转集合
    return list.toArray(new Long[pList.size()]);
}

//递归获取完整路径
private ArrayList<Long> getPath(ArrayList<Long> list,Long sId){
    CategoryEntity pCategory = this.getById(sId);
    list.add(sId);
    //如果该分类父分类不为0,说明上面还有分类,继续递归查找。
    if (pCategory.getParentCid()!=0){
        getPath(list,pCategory.getParentCid());
    }
    return list;
}

测试

@RunWith(SpringRunner.class)
@SpringBootTest()
public class GulimallProductApplicationTests {
    @Autowired
    CategoryService categoryService;

    @Test
    public void testPath(){
        Long[] path = categoryService.findPath(225L);
        System.out.println(Arrays.asList(path));
    }
}

属性分组实体类

  • 添加完整路径属性
 @TableField(exist =false)
 private Long[] catelogPath;

重启项目,前端点击修改按钮。查看后端返回结果。

3.1.2 前端修改展示内容

  1. 修改接收后端传递数据
dataForm: {
  attrGroupId: 0,
  attrGroupName: "",
  sort: "",
  descript: "",
  icon: "",
  catelogId:0,
  catelogPath:[]
},
------------------------------------------------
init(id) {
    this.dataForm.attrGroupId = id || 0;
    this.visible = true;
    this.$nextTick(() => {
      this.$refs["dataForm"].resetFields();
      if (this.dataForm.attrGroupId) {
        this.$http({
          url: this.$http.adornUrl(
            `/product/attrgroup/info/${this.dataForm.attrGroupId}`
          ),
          method: "get",
          params: this.$http.adornParams(),
        }).then(({ data }) => {
          if (data && data.code === 0) {
            this.dataForm.attrGroupName = data.data.attrGroupName;
            this.dataForm.sort = data.data.sort;
            this.dataForm.descript = data.data.descript;
            this.dataForm.icon = data.data.icon;
            this.dataForm.catelogId = data.data.catelogId;
            this.dataForm.catelogPath = data.data.catelogPath;
          }
        });
      }
    });
  },
  1. 修改回显双向绑定属性
<el-form-item label="所属分类id" prop="catelogId">
  <el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props"></el-cascader>
</el-form-item>

再次点击修改,回显成功。

  1. 当关闭修改页面时,删除catelogPath数据。防止下次点击新增,会带有上次修改的数据。
<el-dialog :title="!dataForm.attrGroupId ? '新增' : '修改'" :close-on-click-modal="false" :visible.sync="visible" @closed="dialogClose">

//关闭页面时回调函数
dialogClose(){
   this.dataForm.catelogPath=[]
}
  1. 为新增属性分组添加搜索功能
    filterable placeholder="试试搜索:蓝牙"
<el-form-item label="所属分类id" prop="catelogId">
  <el-cascader v-model="dataForm.catelogPath" :options="categorys" :props="props" filterable placeholder="试试搜索:蓝牙"></el-cascader>
</el-form-item>

效果

4. 品牌功能完善

4.1 品牌管理全局查询功能

@RequestMapping("/list")
//@RequiresPermissions("product:brand:list")
public R list(@RequestParam Map<String, Object> params){
    PageUtils page = brandService.queryPage(params);
    return R.ok().put("page", page);
}
@Override
public PageUtils queryPage(Map<String, Object> params) {
    //1.获取全局搜索关键字
    String key = (String) params.get("key");
    QueryWrapper<BrandEntity> wrapper = new QueryWrapper<BrandEntity>();
    //有关键字,先拼接关键字
    if(!StringUtils.isEmpty(key)){
        //select * from pms_brand where brand_id=key or name like %key%
        wrapper.eq("brand_id",key).or().like("name",key);
    }
    IPage<BrandEntity> page = this.page(
            new Query<BrandEntity>().getPage(params),
            wrapper
    );
    return new PageUtils(page);
}

4.2 品牌管理数据总数展示

@Configuration
@EnableTransactionManagement //开启事务功能,标注事务注解就能生效
@MapperScan("com.atguigu.gulimall.product.dao")
public class MybatisConfig {
    // 旧版 引入分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
         paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
         paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
    // 最新版
//    @Bean
//    public MybatisPlusInterceptor mybatisPlusInterceptor() {
//        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
//        return interceptor;
//    }
}

重启项目测试

5.品牌管理关联分类功能

品牌和分类是多对多关系。

  • 比如:小米品牌关联多个分类,如手机,家用电器等
  • 一个分类也对应多个品牌。比如手机分类对应小米,苹果等品牌。

5.1 查询关联分类接口

点击关联分类,会先查询出该品牌关联的所有分类。

根据接口文档进行开发

controller层

@GetMapping("/catelog/list")
  //@RequiresPermissions("product:categorybrandrelation:list")
  public R catelogList(@RequestParam("brandId") Long brandId){   //前端带一个请求参数brandId
      List<CategoryBrandRelationEntity> data=categoryBrandRelationService.queryByBrandId(brandId);
      return R.ok().put("data", data);
  }

service层

  • 根据品牌id查询关联数据。返回所有和品牌id相同的关联数据并返回。
@Override
public List<CategoryBrandRelationEntity> queryByBrandId(Long brandId) {
    List<CategoryBrandRelationEntity> list = this.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
    return list;
}

5.2 新增关联分类接口

接口文档

  • 前端传递参数只有品牌id和分类id。没有传入品牌名和分类名。但是数据库需要添加这两个字段。用来给前端展示。
  • 如果使用品牌id或分类id去品牌表或分类表进行查询,每次都做表关联查询,会对数据库的性能有较大的影响。
  • 所以在设计表的时候,为关联表添加了两个冗余字段,添加关系id的时候去查询一次品牌名或分类名。之后可以直接从关联表获取品牌名或分类名。

报错

controller层

 @RequestMapping("/save")
//@RequiresPermissions("product:categorybrandrelation:save")
public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
    categoryBrandRelationService.saveDetail(categoryBrandRelation);
    return R.ok();
}

service层

  • 用@Resource代替@AutoWired
@Service("categoryBrandRelationService")
public class CategoryBrandRelationServiceImpl extends ServiceImpl<CategoryBrandRelationDao, CategoryBrandRelationEntity> implements CategoryBrandRelationService {

    @Resource   //这里如果有@AutoWired时会出现注入不成功,用不了该组件
    private CategoryServiceImpl categoryService;
    @Resource
    private BrandServiceImpl brandService;

    @Override
    public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
        //获取品牌名和分类名
        Long brandId = categoryBrandRelation.getBrandId();
        Long catelogId = categoryBrandRelation.getCatelogId();
        BrandEntity brandEntity = brandService.getById(brandId);
        CategoryEntity categoryEntity = categoryService.getById(catelogId);
        categoryBrandRelation.setBrandName(brandEntity.getName());
        categoryBrandRelation.setCatelogName(categoryEntity.getName());
        //保存数据库
        this.save(categoryBrandRelation);
    }
}

5.3 保证关联数据一致性

如:品牌分类关联表中保存有品牌名和品牌id,如果修改品牌表中的品牌id或品牌名,关联表中的数据也应该同步修改。

保证冗余字段的数据一致性。

5.3.1 品牌表修改方法

controller

@RequestMapping("/update")
// @RequiresPermissions("product:brand:update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
    brandService.updateDetail(brand);
    return R.ok();
}

Service

  • @Transactional //修改两个表内容,开启事务。
@Autowired
private CategoryBrandRelationService categoryBrandRelationService;

@Override
@Transactional   //修改两个表内容,开启事务。
public void updateDetail(BrandEntity brand) {
    //先修改品牌表自身数据
    this.updateById(brand);
    //调用关联表方法,修改关联表内容  注入service组件
    categoryBrandRelationService.updataBrand(brand.getBrandId(),brand.getName());
}

5.3.1 关联表修改方法

service

@Override
public void updataBrand(Long brandId, String name) {
    CategoryBrandRelationEntity entity = new CategoryBrandRelationEntity();
    entity.setBrandId(brandId);
    entity.setBrandName(name);
    this.update(entity,new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id",brandId));
}

-----修改分类关联和上面方法一致-----

posted @ 2022-01-26 11:27  初夏那片海  阅读(339)  评论(0)    收藏  举报