直入主题
一、数据库设计
产品表(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);
}