• 通用属性系统设计与实现


        这两年做过不少的小型电商系统,有的卖衣服,有的卖鞋子,有的卖电器,甚至还有些卖虚拟服务的。不同商品的属性千差万别,为了减少以后卖xxx的电商系统的工作量,特将属性系统做成通用版的。
     
    设计思路如下:
    1、可自定义的无限级商品类别。
    2、各类别可自定义属性,属性的类型有:普通文本、数字、价格、单项选择、多项选择、日期、文本域、富文本、图片、布尔值等,添加商品时自动加载所需的组件。
    3、支持公共属性。
    4、支持属性继承,即子类别自动继承父类别的属性,并支持覆盖父类别同名属性。
    5、支持属性值验证,添加商品时对必填项、正则表达式进行自动验证。
    6、支持属性分组,添加商品时属性按照属性分组名进行分组。
     
    模型设计:
     
    Classify:商品类别表
    Attribute:属性表
    AttributeOption:属性选项表,只有类别为“单项选择”和“多项选择”时,属性需要设置属性选项。
    Product:商品表
    ProductAttribute:商品属性关系表
    这里只是对商品属性进行了简单的建模,与属性无关的模型没有画出。
     
    关键代码:
    @{
        ViewBag.Title = "新增产品";
        Layout = "~/Areas/Admin/Views/Shared/_AdminLayout.cshtml";
    }
    @section header{
        <link href="~/Content/css/dataTables.bootstrap.css" rel="stylesheet" type="text/css" />
        <link href="~/Content/css/bootstrap-datetimepicker.min.css" rel="stylesheet" type="text/css" />
        <link href="~/Content/js/plugs/webuploader/webuploader.css" rel="stylesheet" type="text/css" />
    }
    
    <div class="page-container">
        <div class="page-body">
            <div class="row">
                <div class="col-lg-12 col-sm-12 col-xs-12">
                    <div id="simplewizard" class="wizard" data-target="#simplewizard-steps">
                        <ul class="steps">
                            <li data-target="#basicInfoStep" class="active"><span class="step">1</span><span class="title">基础信息</span> <span class="chevron"></span></li>
                            <li data-target="#attributeStep"><span class="step">2</span><span class="title">产品属性</span> <span class="chevron"></span></li>
                            <li data-target="#picInfoStep"><span class="step">3</span><span class="title">产品图片</span> <span class="chevron"></span></li>
                            <li data-target="#confirmInfoStep"><span class="step">4</span><span class="title">确认信息</span> <span class="chevron"></span></li>
                        </ul>
                    </div>
    
                    <div class="step-content" id="simplewizard-steps">
                        <!--基础信息-->
                        <div class="step-pane active" id="basicInfoStep">
                            <form class="form-horizontal" role="form">
                                <div class="form-group">
                                    <label for="name" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>产品名称:</label>
                                    <div class="col-sm-6">
                                        <input type="text" class="form-control" id="name" v-model="product.name">
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label for="originPrice" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>原价:</label>
                                    <div class="col-sm-6">
                                        <input type="text" class="form-control" id="price" v-model="product.originPrice" data-type="2">
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label for="price" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>销售价:</label>
                                    <div class="col-sm-6">
                                        <input type="text" class="form-control" id="price" v-model="product.price" data-type="2">
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label for="inventory" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>库存:</label>
                                    <div class="col-sm-6">
                                        <input type="text" class="form-control" id="inventory" v-model="product.inventory" data-type="2">
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label for="isOnShelf" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>是否上架:</label>
                                    <div class="col-sm-6">
                                        <select id="isOnShelf" v-model="product.isOnShelf">
                                            <option value="false"></option>
                                            <option value="true"></option>
                                        </select>
                                    </div>
                                </div>
                                <div class="form-group">
                                    <label for="classifyId" class="col-sm-2 control-label no-padding-right"><span style="color:red;">* </span>所属分类:</label>
                                    <div class="col-sm-6">
                                        <select id="classifyId" v-model="product.classifyId" v-on:change="classifyChange()" disabled="disabled">
                                            <option v-for="option in classifies" v-bind:value="option.Value">
                                                {{ option.Name }}
                                            </option>
                                        </select>
                                    </div>
                                </div>
                            </form>
                        </div>
    
                        <!--产品属性-->
                        <div class="step-pane" id="attributeStep">
                            <div class="row">
                                <div class="col-sm-12">
                                    <div class="tabbable">
                                        <ul class="nav nav-tabs tabs-flat">
                                            <template v-for="(index,group) in product.groupAttributes">
                                                <li class="tab-sky">
                                                    <a data-toggle="tab" href="#group{{index}}" aria-expanded="true">
                                                        {{group.groupName}}
                                                    </a>
                                                </li>
                                            </template>
                                        </ul>
    
                                        <div class="tab-content  tabs-flat">
                                            <template v-for="(index,group) in product.groupAttributes">
                                                <div id="group{{index}}" class="tab-pane" style="99%">
                                                    <form class="form-horizontal" role="form">
                                                        <template v-for="attribute in group.attributes">
                                                            <div class="form-group">
                                                                <label class="col-sm-2 control-label no-padding-right"><span v-if="attribute.isRequired" style="color:red;">* </span>{{attribute.name}}:</label>
                                                                <div class="col-sm-6">
                                                                    <!--单选-->
                                                                    <select v-if="attribute.attributeType==4" class="form-control" id="atrribute_{{attribute.id}}" v-model="attribute.attributeOptionId">
                                                                        <option v-for="item in attribute.options" v-bind:value="item.value">{{item.name}}</option>
                                                                    </select>
                                                                    <template v-else>
                                                                        <!--多选-->
                                                                        <div v-if="attribute.attributeType==5" class="row">
                                                                            <div v-for="item in attribute.options" class="col-sm-3 col-lg-2">
                                                                                <div class="checkbox">
                                                                                    <label>
                                                                                        <input type="checkbox" v-bind:value="item.value" v-model="attribute.attributeOptionIds">
                                                                                        <span class="text">{{item.name}}</span>
                                                                                    </label>
                                                                                </div>
                                                                            </div>
                                                                        </div>
                                                                        <template v-else>
                                                                            <!--文本域-->
                                                                            <textarea v-if="attribute.attributeType==7" class="form-control" data-type="{{attribute.attributeType}}" v-model="attribute.value"></textarea>
                                                                            <template v-else>
                                                                                <!--富文本-->
                                                                                <script v-if="attribute.attributeType==8" id="atrribute_{{attribute.id}}" data-type="{{attribute.attributeType}}" name="content" type="text/plain">
                                                                                </script>
                                                                                <template v-else>
                                                                                    <!--图片-->
                                                                                    <template v-if="attribute.attributeType==9">
                                                                                        <img style="160px;height:90px;" id="img_{{attribute.id}}" v-bind:src="attribute.value" />
                                                                                        <div id="upload_{{attribute.id}}" data-type="{{attribute.attributeType}}">选择图片</div>
                                                                                    </template>
                                                                                    <input v-else type="text" class="form-control" id="atrribute_{{attribute.id}}" data-type="{{attribute.attributeType}}" v-model="attribute.value">
                                                                                </template>
                                                                            </template>
                                                                        </template>
                                                                    </template>
                                                                </div>
                                                                <div class="col-sm-2" style="margin-top:7px;">{{attribute.tips}}</div>
                                                            </div>
                                                        </template>
                                                    </form>
                                                </div>
                                            </template>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
    
                        <!--产品图片-->
                        <div class="step-pane" id="picInfoStep">
                            <form class="form-horizontal form-bordered" role="form">
                                <div class="form-group">
                                    <div id="upload_album" class="col-sm-2 control-label no-padding-right">上传图片</div>
                                    <div class="col-sm-6">
                                        <div class="row">
                                            <div class="col-sm-3" v-for="path in product.albums">
                                                <img style="160px;height:90px;" id="img_album" v-bind:src="path" />
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </form>
                        </div>
    
                        <!--确认信息-->
                        <div class="step-pane" id="confirmInfoStep">
                            <table class="table table-bordered table-hover">
                                <tbody>
                                    <tr>
                                        <td width="150px">商品名称</td>
                                        <td>{{product.name}}</td>
                                    </tr>
                                    <tr>
                                        <td width="150px">原价</td>
                                        <td>{{product.originPrice}}</td>
                                    </tr>
                                    <tr>
                                        <td width="150px">销售价</td>
                                        <td>{{product.price}}</td>
                                    </tr>
                                    <tr>
                                        <td width="150px">库存</td>
                                        <td>{{product.inventory}}</td>
                                    </tr>
                                    <tr>
                                        <td width="150px">是否上架</td>
                                        <td>{{product.isOnShelf}}</td>
                                    </tr>
                                    <tr>
                                        <td width="150px">所属城市</td>
                                        <td>{{product.regionId}}</td>
                                    </tr>
                                    <template v-for="group in product.groupAttributes">
                                        <tr v-for="attribute in group.attributes">
                                            <td width="150px">{{attribute.name}}</td>
                                            <td>{{{attribute.value}}}</td>
                                        </tr>
                                    </template>
                                    <tr>
                                        <td width="150px">产品图片</td>
                                        <td>
                                            <img v-for="path in product.albums" style="160px;height:90px;" v-bind:src="path" />
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
    
                    <div class="actions actions-footer" id="simplewizard-actions">
                        <div class="btn-group">
                            <button type="button" class="btn btn-default btn-prev"> <i class="fa fa-angle-left"></i>上一步</button>
                            <button type="button" class="btn btn-default btn-next">下一步<i class="fa fa-angle-right"></i></button>
                        </div>
    
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    @section footer{
        <script src="~/Content/js/bode/bode.wizard.js" type="text/javascript"></script>
    
        <script src="~/Content/js/plugs/datetime/bootstrap-datetimepicker.min.js" type="text/javascript"></script>
        <script src="~/Content/js/plugs/datetime/bootstrap-datetimepicker.zh-CN.js" type="text/javascript"></script>
        <script src="~/Content/js/plugs/webuploader/webuploader.js" type="text/javascript"></script>
        <script src="~/Content/js/plugs/ueditor/ueditor.config.js" type="text/javascript"></script>
        <script src="~/Content/js/plugs/ueditor/ueditor.all.min.js" type="text/javascript"></script>
        <script src="~/Content/js/plugs/textarea/jquery.autosize.js" type="text/javascript"></script>
    
        <script type="text/javascript">
            $(document).ready(function(){
                //$("#simplewizard-steps").height($(window).height() - 160);
                $.bode.tools.input.formatDiscount($("input[data-type='2']"));
    
                var attributeInitialized=false,uploaderInitialized=false;
                var vm = new Vue({
                    el: "#simplewizard-steps",
                    data: {
                        product: {
                            name: "",
                            originPrice:0.00,
                            price: 0.00,
                            inventory:0,
                            cover:"",
                            isOnShelf: "false",
                            classifyId: parseInt("@ViewBag.ClassifyId"),
                            groupAttributes: [],
                            extendAttributes: [],
                            albums:[]
                        },
                        classifies: @Html.Raw(Json.Encode(ViewBag.Classifies))
                    },
                    methods: {
                        classifyChange:function(){
                            var self=this;
                            if(!self.product.classifyId)return;
                            $.bode.ajax("/api/services/product/attributes/GetClassifyGroupAttributes",{id:parseInt(self.product.classifyId)},function(gruops){
                                self.product.groupAttributes=gruops;
                                $("script[data-type='6']").each(function(){
                                    var id=$(this).attr("id");
                                    UE.getEditor(id).destroy();
                                });
                                attributeInitialized=false;
                            });
                        },
                        deleteAlbum:function(path){
    
                        }
                    },
                    created: function () {
                        var self=this;
                        $.bode.ajax("/api/services/product/attributes/GetClassifyGroupAttributes",{id:parseInt("@ViewBag.ClassifyId")},function(gruops){
                            self.product.groupAttributes=gruops;
                        });
                    }
                });
    
                var initUploader=function(pick,func){
                    var uploader = WebUploader.create({
                        auto: true,// 选完文件后,是否自动上传。
                        swf: '/Content/js/plugs/webuploader/Uploader.swf',// swf文件路径
                        server: "/api/File/UploadPic",// 文件接收服务端。
                        pick: pick,
                        accept: {
                            title: 'Images',
                            extensions: 'jpg,jpeg,png',
                            mimeTypes: 'image/jpg,image/jpeg,image/png'
                        }
                    });
                    uploader.on("uploadSuccess", function (file, resp) {
                        func(this,resp);
                    });
                }
    
                //初始化wizard插件
                var wizard = new $.bode.wizard("#simplewizard", {
                    onNextClick: function() {
                        var stepName = $("#simplewizard-steps").find(".active").attr("id");
                        if (stepName === "basicInfoStep") {
                            //验证必填项
                            if(!vm.product.name){
                                layer.msg("商品名称不能为空");
                                return false;
                            }
                            if(vm.product.originPrice<=0){
                                layer.msg("原价必须大于0");
                                return false;
                            }
                            if(vm.product.price<=0){
                                layer.msg("售价必须大于0");
                                return false;
                            }
                            if(vm.product.regionId<=0){
                                layer.msg("请选择有效的城市");
                                return false;
                            }
    
                            setTimeout(function(){
                                $("#attributeStep li.tab-sky:eq(0)>a").click();
                                if(!attributeInitialized){
                                    //初始化属性控件
                                    $.bode.tools.input.formatDiscount($("input[data-type='2']"));
                                    $.bode.tools.input.formatDiscount($("input[data-type='3']"));
                                    $.bode.tools.input.formatTime($("input[data-type='6']"));
                                    $("textarea[data-type='7']").autosize({ append: "\n" });
    
                                    $("script[data-type='8']").each(function(){
                                        var id=$(this).attr("id");
                                        UE.getEditor(id);
                                    });
                                    $("div[data-type='9']").each(function(){
                                        initUploader('#'+$(this).attr("id"),function(uploader,resp){
                                            $(uploader.options.pick.replace("upload","img")).attr("src", resp);
                                        });
                                    });
                                    attributeInitialized=true;
                                }
                            },400);
                        }else if (stepName === "attributeStep") {
                            for(var i=0,iLen=vm.product.groupAttributes.length;i<iLen;i++){
                                var group=vm.product.groupAttributes[i];
                                for(var j=0,jLen=group.attributes.length;j<jLen;j++){
                                    var attribute=group.attributes[j];
                                    //对富文本属性进行赋值
                                    if(attribute.attributeType===8){
                                        var id="atrribute_"+attribute.id;
                                        attribute.value=UE.getEditor(id).getContent();
                                    }
    
                                    //验证属性值
                                    var valueField=attribute.attributeType===4?"attributeOptionId":attribute.attributeType===5?"attributeOptionIds":"value";
                                    if(attribute.isRequired&&(attribute[valueField]===""||attribute[valueField]===null)){
                                        layer.msg(""+group.groupName+"】-【"+attribute.name+"】不能为空");
                                        return false;
                                    }
                                    if(attribute.validateRegular){
                                        var reg=eval("("+attribute.validateRegular+")");
                                        if(!reg.test(attribute[valueField])){
                                            layer.msg(""+group.groupName+"】-【"+attribute.name+"】验证失败");
                                            return false;
                                        }
                                    }
                                }
                            }
    
                            if(!uploaderInitialized){
                                setTimeout(function(){
                                    //初始化图片上传控件
                                    initUploader("#upload_album",function(uploader,resp){
                                        vm.product.albums.push(resp);
                                    });
                                    uploaderInitialized=true;
                                },10);
                            }
                        }
                        return true;
                    },
                    onPreClick:function(){
                        var stepName = $("#simplewizard-steps").find(".active").attr("id");
                        if(stepName === "picInfoStep"){
                            setTimeout(function(){
                                $("#attributeStep li.tab-sky:eq(0)>a").click();
                                uploaderInitialized=true;
                            },400);
                        }
                        return true;
                    },
                    onFinish: function() {
                        $.bode.ajax("/api/services/product/products/CreateProduct",vm.product,function(){
                            layer.msg("保存成功");
                        });
    
                        return false;
                    }
                });
            });
        </script>
    }
    新增商品页面
    /// <inheritdoc/>
            public async Task CreateProduct(OperableProductDto input)
            {
                input.CheckNotNull("input");
                input.ClassifyId.CheckGreaterThan("input.ClassifyId", 0);
                if (!_classifyRepository.CheckExists(p => p.Id == input.ClassifyId))
                {
                    throw new UserFriendlyException("指定的分类不存在");
                }
    
                var product = input.MapTo<Domain.Product>();
    
                if (input.IsOnShelf)
                {
                    product.OnShelfTime = DateTime.Now;
                }
                foreach (var group in input.GroupAttributes)
                {
                    foreach (var item in group.Attributes)
                    {
                        product.Attributes.Add(new ProductAttributeMap
                        {
                            AttributeId = item.Id,
                            Value = item.Value,
                            AttributeOptionIds = item.AttributeType == ProductAttributeType.Switch
                            ? FormatOptionIds(item.attributeOptionId)
                            : item.AttributeType == ProductAttributeType.Multiple ? FormatOptionIds(item.attributeOptionIds.ExpandAndToString()) : ""
                        });
                    }
                }
                product.Assets = input.Albums.Select(p => new ProductAsset
                {
                    Path = p,
                    AssetType = AssetType.Picture
                }).ToList();
    
                await _productRepository.InsertAsync(product);
            }
    新增商品数据保存
    {
      "groupAttributes": [
        {
          "groupName": "string",
          "attributes": [
            {
              "name": "string",
              "tips": "string",
              "value": "string",
              "attributeOptionId": "string",
              "attributeOptionIds": [
                "string"
              ],
              "options": [
                {
                  "name": "string",
                  "value": "string"
                }
              ],
              "validateRegular": "string",
              "groupName": "string",
              "isRequired": true,
              "attributeType": 1,
              "id": 0
            }
          ]
        }
      ],
      "albums": [
        "string"
      ],
      "name": "string",
      "originPrice": 0,
      "price": 0,
      "inventory": 0,
      "isOnShelf": true,
      "regionId": 0,
      "classifyId": 0,
      "id": 0
    }
    前端提交Json格式

    示例源码:https://github.com/liuxx001/BodeAbp

     
    展示效果:
     
    属性列表:
    属性选项列表:
     
    新增商品:
     
    写在最后:
    这种属性设计适用范围很广,几乎所有事物都可以使用属性来描述,比如新闻系统中的新闻,论坛中的帖子等等其实都可以用到。园子里有很多关于电商系统属性的设计,但几乎都只有模型。最近工作涉及到这一块,索性就将自己的设计思路与实现过程粗略的写出来,以供交流。
     
     
     
     
     
  • 相关阅读:
    面试官:Redis 有哪些拓展方案?
    面试官:为什么要合并 HTTP 请求?
    Java 调用第三方接口,实战来了!
    Java 如何模拟真正的并发请求?
    如何搭建一台永久运行的个人服务器?试试这个黑科技!
    vs2005 sp1 出来啦!!
    2007年第一帖
    xp pro sp2支持多个用户同时终端连接
    msn中实现 "添加一个活动或游戏邀请"
    softether
  • 原文地址:https://www.cnblogs.com/liuyh/p/5974697.html
Copyright © 2020-2023  润新知