# 业务开发-基础业务-属性管理
## 1.SKU和SPU介绍
商城系统中的商品信息肯定避免不了SPU和SKU这两个概念
### 1.1 SKU和SPU关系
**SPU** = Standard Product Unit (标准化产品单元)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
**SKU**=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。
举个例子:
购买手机的时候,你可以选择华为Mate40系列手机,Mate40系列手机的生产制造商是华为,品牌是华为,手机分类也是华为,不过Mate40系列手机有多款,比如 Mate40 、Mate40 Pro 、 Mate40 Pro +,每款手机的价格也不一样,颜色也不一定一样,那么这个例子中哪些是Spu哪些是Sku呢?
Spu:
```
手机系列:Mate40系列
厂家:华为
品牌:华为
分类:手机
```
Sku:
```
价格
颜色
网络格式
```

程序员的世界中SPU是类,SKU是对象。
### 1.2 基本属性和销售属性
**基本属性**
基本属性就是SPU对应的属性,也就是SKU他们都有的属性,在Java中可以看成static类型的属性,和类绑定
**销售属性**
就是SKU特有的属性,在Java中可以看成私有的属性,属于对象。
每个分类下的商品共享规格参数和销售属性,有些商品不一定要这个分类下的全部的属性。
- 属性是三级分类组织起来的
- 规格参数中有些会提供检索
- 规格参数也是基本属性,有自己的分组
- 属性的分组也是三级分类组织起来的
- 属性名称是确定的,但是值会有不同

## 2. 属性管理
### 2.1 维护菜单
基本的菜单维护咱们以及通过两个案例讲解过了,后面的菜单我们统一的创建出来。将如下的数据插入到renren-fast数据库的sys_menu表中,需要先把之前的数据清空。

```sql
insert into `sys_menu`(`menu_id`,`parent_id`,`name`,`url`,`perms`,`type`,`icon`,`order_num`) values (1,0,'系统管理',NULL,NULL,0,'system',0),(2,1,'管理员列表','sys/user',NULL,1,'admin',1),(3,1,'角色管理','sys/role',NULL,1,'role',2),(4,1,'菜单管理','sys/menu',NULL,1,'menu',3),(5,1,'SQL监控','http://localhost:8080/renren-fast/druid/sql.html',NULL,1,'sql',4),(6,1,'定时任务','job/schedule',NULL,1,'job',5),(7,6,'查看',NULL,'sys:schedule:list,sys:schedule:info',2,NULL,0),(8,6,'新增',NULL,'sys:schedule:save',2,NULL,0),(9,6,'修改',NULL,'sys:schedule:update',2,NULL,0),(10,6,'删除',NULL,'sys:schedule:delete',2,NULL,0),(11,6,'暂停',NULL,'sys:schedule:pause',2,NULL,0),(12,6,'恢复',NULL,'sys:schedule:resume',2,NULL,0),(13,6,'立即执行',NULL,'sys:schedule:run',2,NULL,0),(14,6,'日志列表',NULL,'sys:schedule:log',2,NULL,0),(15,2,'查看',NULL,'sys:user:list,sys:user:info',2,NULL,0),(16,2,'新增',NULL,'sys:user:save,sys:role:select',2,NULL,0),(17,2,'修改',NULL,'sys:user:update,sys:role:select',2,NULL,0),(18,2,'删除',NULL,'sys:user:delete',2,NULL,0),(19,3,'查看',NULL,'sys:role:list,sys:role:info',2,NULL,0),(20,3,'新增',NULL,'sys:role:save,sys:menu:list',2,NULL,0),(21,3,'修改',NULL,'sys:role:update,sys:menu:list',2,NULL,0),(22,3,'删除',NULL,'sys:role:delete',2,NULL,0),(23,4,'查看',NULL,'sys:menu:list,sys:menu:info',2,NULL,0),(24,4,'新增',NULL,'sys:menu:save,sys:menu:select',2,NULL,0),(25,4,'修改',NULL,'sys:menu:update,sys:menu:select',2,NULL,0),(26,4,'删除',NULL,'sys:menu:delete',2,NULL,0),(27,1,'参数管理','sys/config','sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete',1,'config',6),(29,1,'系统日志','sys/log','sys:log:list',1,'log',7),(30,1,'文件上传','oss/oss','sys:oss:all',1,'oss',6),(31,0,'商品系统','','',0,'editor',0),(32,31,'分类维护','product/category','',1,'menu',0),(34,31,'品牌管理','product/brand','',1,'editor',0),(37,31,'平台属性','','',0,'system',0),(38,37,'属性分组','product/attrgroup','',1,'tubiao',0),(39,37,'规格参数','product/baseattr','',1,'log',0),(40,37,'销售属性','product/saleattr','',1,'zonghe',0),(41,31,'商品维护','product/spu','',0,'zonghe',0),(42,0,'优惠营销','','',0,'mudedi',0),(43,0,'库存系统','','',0,'shouye',0),(44,0,'订单系统','','',0,'config',0),(45,0,'用户系统','','',0,'admin',0),(46,0,'内容管理','','',0,'sousuo',0),(47,42,'优惠券管理','coupon/coupon','',1,'zhedie',0),(48,42,'发放记录','coupon/history','',1,'sql',0),(49,42,'专题活动','coupon/subject','',1,'tixing',0),(50,42,'秒杀活动','coupon/seckill','',1,'daohang',0),(51,42,'积分维护','coupon/bounds','',1,'geren',0),(52,42,'满减折扣','coupon/full','',1,'shoucang',0),(53,43,'仓库维护','ware/wareinfo','',1,'shouye',0),(54,43,'库存工作单','ware/task','',1,'log',0),(55,43,'商品库存','ware/sku','',1,'jiesuo',0),(56,44,'订单查询','order/order','',1,'zhedie',0),(57,44,'退货单处理','order/return','',1,'shanchu',0),(58,44,'等级规则','order/settings','',1,'system',0),(59,44,'支付流水查询','order/payment','',1,'job',0),(60,44,'退款流水查询','order/refund','',1,'mudedi',0),(61,45,'会员列表','member/member','',1,'geren',0),(62,45,'会员等级','member/level','',1,'tubiao',0),(63,45,'积分变化','member/growth','',1,'bianji',0),(64,45,'统计信息','member/statistics','',1,'sql',0),(65,46,'首页推荐','content/index','',1,'shouye',0),(66,46,'分类热门','content/category','',1,'zhedie',0),(67,46,'评论管理','content/comments','',1,'pinglun',0),(68,41,'spu管理','product/spu','',1,'config',0),(69,41,'发布商品','product/spuadd','',1,'bianji',0),(70,43,'采购单维护','','',0,'tubiao',0),(71,70,'采购需求','ware/purchaseitem','',1,'editor',0),(72,70,'采购单','ware/purchase','',1,'menu',0),(73,41,'商品管理','product/manager','',1,'zonghe',0),(74,42,'会员价格','coupon/memberprice','',1,'admin',0),(75,42,'每日秒杀','coupon/seckillsession','',1,'job',0);
```
### 2.2 属性组维护
#### 2.2.1 属性组的页面
根据url提示 `product-attrgroup`那么我们需要对应的创建


#### 2.2.2 页面布局
在整个属性组中我们需要通过分类来管理属性组的信息,这时我们需要将业务布局为两部分,这时我们可以借助ElementUI中的Layout来实现

具体的代码实现
```javascript
三级分类
表格
```
页面效果

#### 2.2.3 三级分类组件抽取
三级分类的展示我们后面在多个菜单中都需要使用到。这时我们可以把这个功能抽取出来为一个独立的组件。实现复用。在product目录的同级下创建一个复用的目录common,然后在其中创建Category.vue
```javascript
```

然后在属性组中我们来使用这个组件,引用其他组件的步骤有三个
1> 通过import 来引入组件

2> 在components中注册我们引入的组件

3> 在页面中使用组件

然后我们就可以查看具体的效果了

#### 2.2.4 属性组表单
我们需要在右侧展示一个属性组维护的表单,那么我们只需要将代码生成器中生成的属性组的相关的代码拷贝进来即可。
```javascript
```
然后我们来看下展示的效果

#### 2.2.5 父子组件传值
当我们点击分类菜单的节点的时候,对应需要操作属性组的table

1>我们需要触发Tree节点的点击事件。

2> 我们需要在父组件中定义的一个事件。

3>在子组件中触发父组件中绑定的事件,实现数据的传递
触发事件会回调对应的方法

最后通过演示效果来展示

#### 2.2.6 属性组的展示
我们通过点击分类节点来展示对应的属性组的信息。首先注意分组信息
```json
{
page:1, // 当前页
limit:10, // 每页显示的条数
sidx:"id", // 排序的字段
order:"asc/desc", // 排序的方式
key:"小米" // 查询的关键字
}
```
然后是后端接口的处理,Controller中的处理
```java
@RequestMapping("/list/{catelogId}")
//@RequiresPermissions("product:attrgroup:list")
public R list(@RequestParam Map params
,@PathVariable("catelogId") Long catelogId){
// PageUtils page = attrGroupService.queryPage(params);
PageUtils page = attrGroupService.queryPage(params,catelogId);
return R.ok().put("page", page);
}
```
service中的方法处理
```java
/**
* 查询列表数据
* 根据列表编号来查询
* @param params
* @param catelogId 如何catelogId为0 就不根据catelogId来查询
* @return
*/
@Override
public PageUtils queryPage(Map params, Long catelogId) {
// 获取检索的关键字
String key = (String) params.get("key");
QueryWrapper wrapper = new QueryWrapper<>();
if(!StringUtils.isEmpty(key)){
// 拼接查询的条件
wrapper.and((obj)->{
obj.eq("attr_group_id",key).or().like("attr_group_name",key);
});
}
if(catelogId == 0){
// 不根据catelogId来查询
IPage page = this.page(
new Query().getPage(params),wrapper
);
return new PageUtils(page);
}
// 根据类别编号来查询属性信息
wrapper.eq("catelog_id",catelogId);
IPage page = this.page(
new Query().getPage(params),wrapper
);
return new PageUtils(page);
}
```
前端代码的处理

然后测试效果如下:

#### 2.2.7 属性组添加
点击添加按钮弹出对话框,我们需要对属性组的类别做级联选择操作。

然后维护类别的级联查找

具体的代码实现


在生命周期的方法中填充数据

通过props来指定特定的value和lable

还有就是在级联中的childrens为空的情况,在后端通过@JsonInclude注解来解决

然后对应的效果为

当我们提交表单数据后出现了如下的异常。

我们期望提交的数据是385,要对这个数组数据做一个处理。

然后在提交数据的位置获取最小的类别编号提交就可以了

然后提交表单就可以了。

#### 2.2.8 属性组修改
在更新属性组的信息的时候,因为三级分类的信息展示需要为[2,22,225]这种方式,而后台只是提供了225,那么为了能够更加清楚的展示相关的信息,我们需要自己来出来下
前端:

后端服务:


完成根据类别编号查询出对应的父组件
```java
@Override
public Long[] findCatelogPath(Long catelogId) {
List paths = new ArrayList<>();
List parentPath = findParentPath(catelogId, paths);
Collections.reverse(parentPath);
return parentPath.toArray(new Long[parentPath.size()]);
}
/**
* 225,22,2
* @param catelogId
* @param paths
* @return
*/
private List findParentPath(Long catelogId,List paths){
paths.add(catelogId);
CategoryEntity entity = this.getById(catelogId);
if(entity.getParentCid() != 0){
findParentPath(entity.getParentCid(),paths);
}
return paths;
}
```

## 3.属性组和基本属性关联
### 3.1 属性组对应的属性信息的查询
前面我们以及维护好了属性组的信息和基本属性信息,现在我们需要把它们关联起来。

然后对应的我们需要关联来处理,实现的效果

前端的实现不再赘述,自己查看代码,后端我们需要新增对应的接口

然后就是具体的service实现
```java
/**
* 根据属性组编号查询对应的基本信息
* @param attrgroupId
* @return
*/
@Override
public List getRelationAttr(Long attrgroupId) {
// 1. 根据属性组编号从 属性组和基本信息的关联表中查询出对应的属性信息
List list = attrAttrgroupRelationDao
.selectList(new QueryWrapper().eq("attr_group_id", attrgroupId));
// 2.根据属性id数组获取对应的详情信息
List attrEntities = list.stream()
.map((entity) -> this.getById(entity.getAttrId()))
.filter((entity)-> entity != null)
.collect(Collectors.toList());
return attrEntities;
}
```
### 3.2 解除关联
效果是在关联table中我们点击移除可以解除这个关联的效果
前端只需要把删除的信息提交给后端接口就可以了。后端我们需要创建VO对象来接收数据

创建对应的Controller方法,接收和处理该请求

然后service中处理

我们需要自己通过对应的SQL语句来批量的删除关联关系

### 3.3 未关联属性查询
我们希望在属性组中直接对关联的属性做关联操作。这时我们就需要先查询出没有被关联的属性信息。
首先我们需要在Controller新增查询未关联属性的方法

添加对应的业务处理service方法
```java
@Override
public PageUtils getNoAttrRelation(Map params, Long attrgroupId) {
// 1.查询当前属性组所在的类别编号
AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrgroupId);
// 获取到对应的分类id
Long catelogId = attrGroupEntity.getCatelogId();
// 2.当前分组只能关联自己所属的类别下其他的分组没有关联的属性信息。
// 先找到这个类别下的所有的分组信息
List group = attrGroupDao.selectList(new QueryWrapper().eq("catelog_id", catelogId));
// 获取属性组的编号集合
List groupIds = group.stream().map((g) -> g.getAttrGroupId()).collect(Collectors.toList());
// 然后查询出类别信息下所有的属性组已经分配的属性信息
List relationEntities = attrAttrgroupRelationDao.selectList(new QueryWrapper().in("attr_group_id", groupIds));
List attrIds = relationEntities.stream().map((m) -> m.getAttrId()).collect(Collectors.toList());
// 根据类别编号查询所有的属性信息并排除掉上面的属性信息即可
// 这其实就是需要查询出最终返回给调用者的信息了 分页 带条件查询
QueryWrapper wrapper = new QueryWrapper()
.eq("catelog_id",catelogId)
// 查询的是基本属性信息,不需要查询销售属性信息
.eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
// 然后添加排除的条件
if(attrIds != null && attrIds.size() > 0){
wrapper.notIn("attr_id",attrIds);
}
// 还有根据key的查询操作
String key = (String)params.get("key");
if(!StringUtils.isEmpty(key)){
wrapper.and((w)->{
w.eq("attr_id",key).or().like("attr_name",key);
});
}
// 查询对应的相关信息
IPage page = this.page(
new Query().getPage(params),
wrapper
);
return new PageUtils(page);
}
```
然后最后的显示效果

### 3.4 确认新增
完成关联关系的存储。
在Controller中新增处理请求的方法

然后在service中处理批量插入的需求

然后实现的效果

