直入主题

一、数据库设计

 产品表(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);
}
posted on 2021-01-23 15:23  浅灰色的记忆  阅读(801)  评论(0编辑  收藏  举报