第一种设计
解决方案是,采用json来保存整个规格参数模板,不需要额外的表,一个字符串就够了
以主芯片这一组为例:
group:注明,这里是主芯片
params:该组的所有规格属性,因为不止一个,所以是一个数组。这里包含四个规格属性:
CPU品牌,CPU型号,CPU频率,CPU核数。每个规格属性都是一个对象,包含以下信息:
k:属性名称
searchable:是否作为搜索字段,将来在搜索页面使用,boolean类型
global:是否是SPU全局属性,boolean类型。true为全局属性,false为SKU的特有属性
options:属性值的可选项,数组结构。起约束作用,不允许填写可选项以外的值,比如CPU核数,有人添10000核岂不是很扯淡
numerical:是否为数值,boolean类型,true则为数值,false则不是。为空也代表非数值
unit:单位,如:克,毫米。如果是数值类型,那么就需要有单位,否则可以不填。
总结下:
规格参数分组,每组有多个参数
参数的 k代表属性名称,没有值,具体的SPU才能确定值
参数会有不同的属性:是否可搜索,是否是全局、是否是数值,这些都用boolean值进行标记:
SPU下的多个SKU共享的参数称为全局属性,用global标记
SPU下的多个SKU特有的参数称为特有属性
如果参数是数值类型,用numerical标记,并且指定单位unit
如果参数可搜索,用searchable标记
第二种设计
如果按照传统数据库设计,这里至少需要3张表:
spec_group:代表组,与商品分类关联
spec_param:属性名,与组关联商品分类,一对多
param_value==>spu表中:属性备选值,与属性名关联,一对多
这样程序的复杂度大大增加,但是提高了数据的复用性。
把商品的图片存到虚拟机
数据库的image属性值http://image.ayh.com/images/…
在nginx的image.ayh.com配置加 (static目录下有image文件夹)
location / {
root /home/ayh/static;
index index.html index.htm;
}
商品规格数据结构
一个商品分类下有多个规格参数组,一个规格参数组下有多个规格参数,分成2个表
规格参数组表(和商品分类表绑定)
CREATE TABLE `tb_spec_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
`name` varchar(32) NOT NULL COMMENT '规格组的名称',
PRIMARY KEY (`id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
@Table(name = "tb_spec_group")
@Data
public class SpecGroup {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String name;
private Long cid;
}
规格参数表(和商品表绑定)
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(256) NOT NULL COMMENT '参数名',
`numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
`unit` varchar(256) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
`generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
`searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
`segments` varchar(1024) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
PRIMARY KEY (`id`),
KEY `key_group` (`group_id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';
@Table(name = "tb_spec_param")
@Data
public class SpecParam {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private Long gid;
private String name;
private Boolean numeric;//是否为数值类型
private String unit;//数值单位
private Boolean generic;//是否通用
private Boolean searching;//是否可搜索
private String segments;//数值类型参数,如果需要搜索,则添加分段间隔值
}
商品的表结构
商品抽象出两个概念
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品
SPU是一个抽象的商品集概念,为了方便后台的管理。
SKU才是具体要销售的商品,每一个SKU的价格、库存可能会不一样,用户购买的是SKU而不是SPU
SPU表
表结构
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '名称',
`sub_title` varchar(256) 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=195 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象性的商品,比如 iphone8';
把spu表做了垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail
表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`generic_spec` varchar(3000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
`special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(1000) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(1000) DEFAULT '' COMMENT '售后服务',
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
规格参数与商品分类绑定,一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数.
SPU中保存通用的规格参数信息。
SKU中保存特有规格参数。
generic_spec,其中保存通用规格参数信息的值,这里为了方便查询,使用了json格式,
json结构,其中都是键值对:
key:对应的规格参数的spec_param的id
value:对应规格参数的值
special_spec,在SPU中,会把特有属性的所有值都记录下来,形成一个数组,
也是json结构:
key:规格参数id
value: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';
indexes,保存特有规格参数的值的下标。在SPU表中,已经对特有规格参数及可选项进行了保存。
每一个属性值,对应于SPUoptions数组的一个选项,我们记录下角标,将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。
own_spec,保存的是特有属性的键值对。
SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。
还有一张表,代表库存:因为库存字段写频率较高,而SKU的其它字段以读为主,因此我们将两张表分离,读写不会干扰。
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='库存表,代表库存,秒杀库存等信息';
代码
pojo,添加了2个字段,页面发送请求的时候要用的,数据库里面没有的字段,应该写一个vo类,然后使用的时候和po类相互转换。这里省略了。
@Data
@Table(name = "tb_spu")
public class SPU {
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
private String name;
private String subTitle;
private Long bid;
private Long cid1;
private Long cid2;
private Long cid3;
private Boolean saleable;//是否上下架
private Date createTime;
//*******以下为省去vo和po的转换********
@JsonIgnore//页面忽略字段
private Boolean valid;//判断是否已经删除,用于逻辑删除
@JsonIgnore //页面忽略字段
private Date lastUpdateTime;//最后更新时间
@Transient //数据库没有,暂时用于页面,import javax.persistence.Transient
private String bname;
@Transient //数据库没有,暂时用于页面
private String cname;
}
//controller
@RestController
@RequestMapping("spu")
public class GoodsController {
@Autowired
private GoodsService goodsService;
@GetMapping("page")
public ResponseEntity<PageResult<SPU>> queryPageInfo(
@RequestParam( value = "page",defaultValue = "1")Integer page,
@RequestParam( value = "rows",defaultValue = "5")Integer rows,
@RequestParam( value = "key",required = false)String key,
@RequestParam( value = "saleable",required = false)Boolean saleable){
PageResult<SPU> pageResult = goodsService.queryPageInfo(page, rows, key, saleable);
if (pageResult == null || pageResult.getCurrentPageItems().size() == 0) {
throw new AyhException(ExceptionEnum.GOODS_TABLE_DATA_NOT_FOUND);
}
return ResponseEntity.ok(pageResult);
}
@Service
public class GoodsService {
@Autowired
private SPUMapper spuMapper;
@Autowired
private CategoryService categoryService;
@Autowired
private BrandService brandService;
public PageResult<SPU> queryPageInfo(Integer page, Integer rows, String key, Boolean saleable) {
PageHelper.startPage(page, rows);
//过滤添加条件
Example example = new Example(SPU.class);
Example.Criteria criteria = example.createCriteria();
if (StringUtils.isNotBlank(key)) {
criteria.andLike("name","%"+key+"%");
}
if (saleable != null) {
criteria.andEqualTo("saleable", saleable);
}
//按更新时间排序
example.setOrderByClause("last_update_time DESC");
List<SPU> spus = spuMapper.selectByExample(example);
if (CollectionUtils.isEmpty(spus)) {
throw new AyhException(ExceptionEnum.GOODS_TABLE_DATA_NOT_FOUND);
}
spus = loadCategoryNameAndBrandName(spus);
PageInfo<SPU> pageInfo = PageInfo.of(spus);
return new PageResult<SPU>(pageInfo.getTotal(),pageInfo.getList());
}
private List<SPU> loadCategoryNameAndBrandName(List<SPU> spus) {
for (SPU spu : spus) {
//设置商品分类name
List<Category> categories = categoryService.queryByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
List<String> stringList = categories.stream().map(Category::getName).collect(Collectors.toList());
spu.setCname(StringUtils.join(stringList, "/"));
//设置品牌name
spu.setBname(brandService.queryBrandById(spu.getBid()).getName());
}
return spus;
}
}
在categoryService
/**
* 根据分类id的集合 List<>ids 查询分类对象
* @param ids
* @return
*/
public List<Category> queryByIds(List<Long> ids) {
List<Category> list = categoryMapper.selectByIdList(ids);
if (CollectionUtils.isEmpty(list)) {
throw new AyhException(ExceptionEnum.CATEGORY_DATA_NOT_FOUND);
}
return list;
}
//mapper 继承通用Mapper和IdListMapper
public interface CategoryMapper extends Mapper<Category>, IdListMapper<Category,Long> {