直入主題
一、數據庫設計
產品表(t_product):商品主表,上關聯店鋪商戶、分類(merchat_id、category_id),下關聯產品屬性和sku,其中一些字段可根據需求進行變動。
產品屬性表(t_product_attr):商品屬性表,存放產品對應的屬性及規格
產品SKU表(t_product_sku):根據商品屬性和規格,計算排列出所有情況,生成的sku數據列表,返回客戶端后,由客戶端填充對應價格、庫存、logo等參數,交由該表存儲
drop table if exists t_product; /*==============================================================*/ /* Table: t_product */ /*==============================================================*/ create table t_product ( id int(11) not null auto_increment, merchat_id int(11) comment '商戶Id(0為總后臺管理員創建,不為0的時候是商戶后臺創建)', category_id int(11) comment '商品所在的系統分類', name varchar(128) comment '名稱', description varchar(256) comment '描述', bar_code varchar(15) comment '產品條碼(一維碼)', keyword varchar(512) comment '關鍵詞', logo varchar(256) comment '主圖', photo varchar(1024) comment '輪播圖', is_show tinyint default 0 comment '是否上架:0下架,1上架', fictitiou_id tinyint default 0 comment '虛擬id(優惠券id)', fictitiou_type tinyint default 0 comment '虛擬類型:0優惠券,1流量券,2話費券', type tinyint comment '0虛擬,1實體', price double(11, 2) default 0.00 comment '價格', postage double comment '郵費', cost double comment '成本價', ot_price double comment '市場價', integral int default 0 comment '積分', discount double default 0 comment '折扣', max_deduction_integral double(11, 2) default 0.00 comment '積分最大可抵扣金額', start_buy int(11) default 1 comment '起購數量', stock int(11) default 0 comment '庫存', sales int(11) default 0 comment '銷量', attr_result text comment 'attrJSON結果', info_html text comment '詳情圖', param_html text comment '參數圖', is_delete tinyint default 0 comment '是否刪除,0否,1是', is_postage tinyint default 0 comment '是否包郵,0否,1是', is_new tinyint default 0 comment '是否新品,0否,1是', is_recommend tinyint default 0 comment '是否推薦,0否,1是', update_time datetime comment '更新時間', create_time datetime comment '創建時間', primary key (id) ); alter table t_product comment '產品'; drop table if exists t_product_attr; /*==============================================================*/ /* Table: t_product_attr */ /*==============================================================*/ create table t_product_attr ( id int(11) not null auto_increment, product_id int(11) comment '商品id', is_hidden bool default 0 comment '是否隱藏,0否,是', attr_name varchar(20) comment '屬性名稱', attr_values varchar(255) comment '屬性值,逗號拼接', defaul_value varchar(255) comment '默認屬性值', primary key (id) ); alter table t_product_attr comment '產品屬性'; alter table t_product_attr add constraint FK_Reference_58 foreign key (product_id) references t_product (id) on delete restrict on update restrict; drop table if exists t_product_sku; /*==============================================================*/ /* Table: t_product_sku */ /*==============================================================*/ create table t_product_sku ( id int(11) not null auto_increment, product_id int(11) not null comment '商品id', is_default tinyint not null default 0 comment '是否默認,0否,是', logo varchar(256) comment '主圖', suk varchar(128) not null comment '商品屬性索引值 (attr_value|attr_value[|....])', stock int(11) not null default 0 comment '庫存', sales int(11) not null default 0 comment '銷量', cost double not null default 0.00 comment '成本價', price double not null default 0.00 comment '價格', primary key (id) ); alter table t_product_sku comment '商品屬性解析結果'; alter table t_product_sku add constraint FK_Reference_86 foreign key (product_id) references t_product (id) on delete restrict on update restrict;
注意:商品屬性的生成與變更與產品本身隔離處理,產品主表只涉及單純的CRUD,所以此處就不再列出涉及產品相關的代碼
二、服務端邏輯
/** * 解析屬性,得到屬性格式 * @param id * @param jsonStr * @return */ public ResponseVO getAttrFormat(Integer id, String jsonStr) { if (id == null){ return ResponseVO.error("請選擇商品"); } Product product = productMapper.selectByPrimaryKey(id); if (product == null){ return ResponseVO.error("商品不存在"); } AttrDetailDto detailDTO = analysisAttr(jsonStr); List<ProductSkuDto> newList = new ArrayList<>(); for (Map<String, Map<String,String>> map : detailDTO.getRes()) { // 封裝結果 ProductSkuDto productSkuDto = new ProductSkuDto(); productSkuDto.setDetail(map.get("detail")); productSkuDto.setCost(product.getCost()); productSkuDto.setPrice(product.getPrice()); productSkuDto.setSales(product.getSales()); productSkuDto.setLogo(product.getLogo()); newList.add(productSkuDto); } return ResponseVO.success(newList); } /** * 解析屬性規則算法 * @param jsonStr * @return */ public AttrDetailDto analysisAttr(String jsonStr){ JSONObject jsonObject = JSON.parseObject(jsonStr); List<ProductAttrDto> productAttrList = JSONArray.parseArray(jsonObject.get("attrs").toString(), ProductAttrDto.class); // 聲明當前屬性所擁有規格模板 List<String> currentAttrSpecTemp = new ArrayList<>(); List<Map<String,Map<String,String>>> res =new ArrayList<>(); if(productAttrList.size() > 1){ // 如果大于1,說明增加了多個屬性 for (int i=0; i<productAttrList.size()-1; i++){// 遍歷屬性 if(i == 0) { // 如果是第一個屬性 獲取第一個屬性的元素規格列表,給模板temp0進行遍歷操作,如果不是,說明已經拼接過一輪,繼續拼接 currentAttrSpecTemp = productAttrList.get(i).getDetail(); } // 清空初始化模板集合,用于存儲拼接后的規格信息,注意,只能在這個位置初始化,如果拿到上一個for循環之上,下面給將skuTemp實例的引用賦值之后(currentAttrSpecTemp = skuTemp),currentAttrSpecTemp就會因為skuTemp.add(skuDetail);而實時增加數據,導致出現ConcurrentModificationException異常 List<String> skuTemp = new LinkedList<>(); // 遍歷元素規格列表 for (String skuName : currentAttrSpecTemp) { // 當前元素拼接下一個元素 // 將下一個屬性的 元素依次遍歷 List<String> nextAttrSpecTemp = productAttrList.get(i + 1).getDetail(); for (String nextSpecName : nextAttrSpecTemp) { String skuDetail = ""; if(i == 0){ // 如果為0,就使用第一屬性的名稱去拼接第一個規格,用下一個屬性名稱拼接下一個屬性的當前規格 // 顏色_白色-體積_小 skuDetail = productAttrList.get(i).getAttrName() + "_" + skuName + "-" + productAttrList.get(i+1).getAttrName() + "_" + nextSpecName; }else{ // 如果不為0,表示不是第一個屬性,那么就使用之前拼接得到的結果,繼續拼接 // 顏色_白色-體積_小-溫度_冰 skuDetail = skuName + "-" + productAttrList.get(i+1).getAttrName() + "_" + nextSpecName; } // 將解析拼接后的規格存入 skuTemp.add(skuDetail); // 如果是數組中的倒數第二個元素,因為會向后拼接一個元素(productAttrList.get(i+1)),所以此處表示已經拼接結束 if(i == productAttrList.size() - 2){ Map<String,Map<String,String>> skuDetailTemp = new LinkedHashMap<>(); // 將一組結果,拆分成鍵值對的方式存儲 Map<String,String> skuDetailTempKv = new LinkedHashMap<>(); // 根據-分成數組,得到[顏色_白色,體積_小,溫度_冰] List<String> attr_sku_arr = Arrays.asList(skuDetail.split("-")); for (String h : attr_sku_arr) { // _分成數組,得到[顏色,白色] List<String> attrBySpecArr = Arrays.asList(h.split("_")); // 如果大于1,說明屬性有對應的 規格值 if(attrBySpecArr.size() > 1){ // 按照屬性名:規格值的結構存入臨時模板列表 skuDetailTempKv.put(attrBySpecArr.get(0), attrBySpecArr.get(1)); }else{ // 未獲取到屬性名,按空字符串存儲 skuDetailTempKv.put(attrBySpecArr.get(0),""); } } // 得到[顏色:白色,體積:小,溫度:冰] skuDetailTemp.put("detail",skuDetailTempKv); // 將這一組結果存入到最終將要返回的數據中 res.add(skuDetailTemp); } } } // 走到這里,表示第一個元素和第二個元素已經全部生成完畢,將得到的結果列表交給temp0去執行下一趟 if(!skuTemp.isEmpty()){ // 這里只能將自己的參數給這個值,不能將引用也給他,否則在內循環的時候,就會照成java.util.ConcurrentModificationException currentAttrSpecTemp = skuTemp; } } }else{ // 只有一種屬性 List<String> temp0Arr = new ArrayList<>(); // 清空初始化模板集合,用于存儲拼接后的規格信息 List<String> skuTemp = new LinkedList<>(); for (ProductAttrDto productAttr : productAttrList) { for (String str : productAttr.getDetail()) { // 這這種屬性和其中的規格遍歷得到屬性規格鍵值對,不需要拼接下一個屬性 Map<String,Map<String,String>> skuDetailTemp = new LinkedHashMap<>(); //List<Map<String,String>> list1 = new ArrayList<>(); temp0Arr.add(productAttr.getAttrName()+"_"+str); Map<String,String> skuDetailTempKv = new LinkedHashMap<>(); skuDetailTempKv.put(productAttr.getAttrName(),str); //list1.add(map1); skuDetailTemp.put("detail",skuDetailTempKv); res.add(skuDetailTemp); // 將解析拼接后的規格存入 skuTemp.add(productAttr.getAttrName() + "-" + str); } currentAttrSpecTemp = skuTemp; } }// 此時已得到每一組的匹配詳情,(所有可能產生的匹配結果列表),將其已文本,和鍵值對的方式返回 AttrDetailDto detailDTO = new AttrDetailDto(); detailDTO.setData(currentAttrSpecTemp); detailDTO.setRes(res); return detailDTO; } @Transactional(rollbackFor = Exception.class) public ResponseVO productAttrAdd(Integer productId, String jsonStr) { JSONObject jsonObject = JSON.parseObject(jsonStr); // 傳入屬性列表[{"attrName":"顏色","defaulValue":"","isHidden":1,"detail":["白色","黑色"]},{"attrName":"體積","defaulValue":"","isHidden":1,"detail":["小","中","大"]},{"attrName":"溫度","defaulValue":"","isHidden":1,"detail":["冰","常溫"]}] List<ProductAttrDto> attrDtoList = JSON.parseArray( jsonObject.get("attrs").toString(), ProductAttrDto.class); List<ProductSkuDto> valueDtoList = JSON.parseArray(// 傳入解析后的規格詳情列表[{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"小","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"小","溫度":"常溫"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"中","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"中","溫度":"常溫"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"大","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"白色","體積":"大","溫度":"常溫"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"小","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"小","溫度":"常溫"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"中","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"中","溫度":"常溫"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"大","溫度":"冰"},"check":false},{"price":120,"cost":0.2,"sales":52,"logo":"https://image.dayouqiantu.cn/5ca011a1cd487.jpg","detail":{"顏色":"黑色","體積":"大","溫度":"常溫"},"check":false}] jsonObject.get("sku").toString(), ProductSkuDto.class); Product product = productMapper.selectByPrimaryKey(productId); //取最小價格 Double minPrice = product.getPrice(); // 計算庫存 Integer stock = product.getStock(); if(attrDtoList.isEmpty() || valueDtoList.isEmpty()){ return ResponseVO.error("請設置至少一個屬性!"); }else{ //插入之前清空 productAttrClear(product.getId()); } for (ProductAttrDto attrDto : attrDtoList) { ProductAttr attr = new ProductAttr(); // 記錄商品屬性 attr.setProductId(productId); attr.setAttrName(attrDto.getAttrName()); attr.setDefaulValue(attrDto.getDefaulValue()); //根據,號拼接商品屬性下的規格 "大,中,小" String attrValues = ""; for (String valueName : attrDto.getDetail()){ // 如果未指定默認值,那么就使用第一個 if (attr.getDefaulValue() == null || attr.getDefaulValue().length() == 0){ attr.setDefaulValue(valueName); } attrValues += attrValues.length() > 0 ? "," + valueName : valueName; } attr.setAttrValues(attrValues); productAttrMapper.insertSelective(attr); } for (ProductSkuDto attrValuesDTO : valueDtoList) { ProductSku attrValues = new ProductSku();// 記錄商品屬性規格詳情信息 attrValues.setProductId(productId); List<String> stringList = attrValuesDTO.getDetail().values() .stream().collect(Collectors.toList()); Collections.sort(stringList); // 將屬性對應的每一種規格通過,號拼接,存入表中,同一產品唯一 "冰,小,白色" String suk = ""; for (String sukName : stringList){ suk += suk.length() > 0 ? "," + sukName : sukName; } attrValues.setSuk(suk); attrValues.setPrice(attrValuesDTO.getPrice()); attrValues.setCost(attrValuesDTO.getCost()); attrValues.setStock(attrValuesDTO.getSales()); attrValues.setLogo(attrValuesDTO.getLogo()); productSkuMapper.insertSelective(attrValues); // 計算價格 minPrice = minPrice > attrValues.getPrice() ? attrValues.getPrice() : minPrice; // 計算庫存 stock += attrValues.getStock(); } Map<String,Object> map = new LinkedHashMap<>(); map.put("attr",jsonObject.get("attr")); map.put("sku",jsonObject.get("sku")); //設置庫存及價格 product.setPrice(minPrice); product.setStock(stock); product.setAttrResult(JSON.toJSONString(map)); int result = productMapper.updateByPrimaryKeySelective(product); if (result != 1){ return ResponseVO.error("請設置至少一個屬性!"); } return ResponseVO.success(); } /** * 清空屬性及sku * @param productId * @return */ public void productAttrClear(Integer productId) { productAttrMapper.deleteByProductId(productId); productSkuMapper.deleteByProductId(productId); }
三、Controller展示
@ApiOperation(value = "生成屬性") @PostMapping(value = "getAttrFormat") @ApiImplicitParams({ @ApiImplicitParam(name = "productId", value = "商品id", paramType = "query", dataType = "Integer"), @ApiImplicitParam(name = "jsonStr", value = "屬性json格式(轉成swagger注意雙引號中文切換):{\"attrs\":[{\"attrName\":\"顏色\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"白色\",\"黑色\"]},{\"attrName\":\"體積\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"小\",\"中\",\"大\"]},{\"attrName\":\"溫度\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"冰\",\"常溫\"]}],\"sku\":[]}", paramType = "query", dataType = "String") }) public ResponseVO getAttrFormat(Integer productId, String jsonStr){ return productService.getAttrFormat(productId,jsonStr); } @ApiOperation(value = "設置保存屬性") @PostMapping(value = "productAttrAdd") @ApiImplicitParams({ @ApiImplicitParam(name = "productId", value = "商品id", paramType = "query", dataType = "Integer"), @ApiImplicitParam(name = "jsonStr", value = "屬性json格式(轉成swagger注意雙引號中文切換):{\"attrs\":[{\"attrName\":\"顏色\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"白色\",\"黑色\"]},{\"attrName\":\"體積\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"小\",\"中\",\"大\"]},{\"attrName\":\"溫度\",\"defaulValue\":\"\",\"isHidden\":1,\"detail\":[\"冰\",\"常溫\"]}],\"sku\":[{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"小\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"小\",\"溫度\":\"常溫\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"中\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"中\",\"溫度\":\"常溫\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"大\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"白色\",\"體積\":\"大\",\"溫度\":\"常溫\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"小\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"小\",\"溫度\":\"常溫\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"中\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"中\",\"溫度\":\"常溫\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"大\",\"溫度\":\"冰\"}},{\"price\":120,\"cost\":0.2,\"sales\":52,\"logo\":\"https://image.dayouqiantu.cn/5ca011a1cd487.jpg\",\"detail\":{\"顏色\":\"黑色\",\"體積\":\"大\",\"溫度\":\"常溫\"}}]}", paramType = "query", dataType = "String") }) public ResponseVO productAttrAdd(Integer productId, String jsonStr){ return productService.productAttrAdd(productId, jsonStr); } @ApiOperation(value = "清除屬性") @PostMapping(value = "productAttrClear") @ApiImplicitParams({ @ApiImplicitParam(name = "productId", value = "商品id", paramType = "query", dataType = "Integer") }) public ResponseVO clearAttr(Integer productId){ productService.productAttrClear(productId); return ResponseVO.success(); }
四、業務邏輯說明
1、執行步驟:
1)創建產品
2)調用getAttrFormat接口,根據傳入的屬性,生成對應的sku列表
3)前端根據返回的sku列表,展示對應的sku信息,填入對應sku的價格、庫存、logo
4)調用productAttrAdd接口,將封裝好的sku列表 和 用戶輸入的屬性,存入t_product_attr、t_product_sku表中,并更新t_product表中的庫存(所有sku庫存總和)和價格(所有sku中最低價格),將入參轉為json存入主表的attr_result字段中,方便查詢
五、前端頁面效果圖
1、填寫屬性

2、點擊生成,解析屬性得到sku,3、填充或更改logo、金額、庫存、成本價 并 提交

本文參考jeck胡老師的實戰項目源碼,僅供參考,如有不妥,請聯系刪除
{
ProductAttr attr = new ProductAttr(); // 記錄商品屬性
attr.setProductId(productId);
attr.setAttrName(attrDto.getAttrName());
attr.setDefaulValue(attrDto.getDefaulValue());
//根據,號拼接商品屬性下的規格 "大,中,小"
String attrValues = "";
for (String valueName : attrDto.getDetail()){
// 如果未指定默認值,那么就使用第一個
if (attr.getDefaulValue() == null || attr.getDefaulValue().length() == 0){
attr.setDefaulValue(valueName);
}
attrValues += attrValues.length() > 0 ? "," + valueName : valueName;
}
attr.setAttrValues(attrValues);
productAttrMapper.insertSelective(attr);
}
不要讓未來的你,來埋怨如今的自己。
浙公網安備 33010602011771號