通用属性系统设计与实现

    这两年做过不少的小型电商系统,有的卖衣服,有的卖鞋子,有的卖电器,甚至还有些卖虚拟服务的。不同商品的属性千差万别,为了减少以后卖xxx的电商系统的工作量,特将属性系统做成通用版的。
 
设计思路如下:
1、可自定义的无限级商品类别。
2、各类别可自定义属性,属性的类型有:普通文本、数字、价格、单项选择、多项选择、日期、文本域、富文本、图片、布尔值等,添加商品时自动加载所需的组件。
3、支持公共属性。
4、支持属性继承,即子类别自动继承父类别的属性,并支持覆盖父类别同名属性。
5、支持属性值验证,添加商品时对必填项、正则表达式进行自动验证。
6、支持属性分组,添加商品时属性按照属性分组名进行分组。
 
模型设计:
  通用属性系统设计与实现_第1张图片
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">1span><span class="title">基础信息span> <span class="chevron">span>li>
                        <li data-target="#attributeStep"><span class="step">2span><span class="title">产品属性span> <span class="chevron">span>li>
                        <li data-target="#picInfoStep"><span class="step">3span><span class="title">产品图片span> <span class="chevron">span>li>
                        <li data-target="#confirmInfoStep"><span class="step">4span><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="width: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="width: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="width: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="width: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>
}
新增商品页面

 

/// 
        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();

            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

 
展示效果:
 
属性列表:
通用属性系统设计与实现_第2张图片
属性选项列表:
通用属性系统设计与实现_第3张图片
 
新增商品:
通用属性系统设计与实现_第4张图片
 
写在最后:
这种属性设计适用范围很广,几乎所有事物都可以使用属性来描述,比如新闻系统中的新闻,论坛中的帖子等等其实都可以用到。园子里有很多关于电商系统属性的设计,但几乎都只有模型。最近工作涉及到这一块,索性就将自己的设计思路与实现过程粗略的写出来,以供交流。
 
 
 
 
 

转载于:https://www.cnblogs.com/liuyh/p/5974697.html

你可能感兴趣的:(json,人工智能,前端)