第一種設計
解決方案是,採用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> {