Orchard商城模块(Commerce)设计与后台部分

前言:使用CMS开发网站为目标,编写一个扩展性比较好的商城模块。

首先是整体流程图,大概介绍功能与设计。

 

接下来我们逐个模块功能介绍。

一。商品管理模块

商品模块中可发布需要在线售卖的商品 (套餐商品)  

1.1 添加一个商品

1. 商品正常价,与当前促销价,  (不填写促销价,将按照正常价计算  。)

2.是否为虚拟商品 (虚拟商品将不需要填写收货地址, 如果购物车上所有商品均为虚拟商品,则不需填写收货地址,如果有一个非虚拟商品,仍需填写)

 

 以下商品实体类

namespace Aivics.Commerce.Models
{
    /// <summary>
    /// 商品对象
    /// </summary>
    [OrchardFeature("Aivics.Commerce")]
    public class ProductPart : ContentPart<ProductPartRecord>, IProduct {
        /// <summary>
        /// 商品SKU
        /// </summary>
        [Required]
        public string Sku
        {
            get { return Retrieve(r => r.Sku); }
            set { Store(r => r.Sku, value); }
        }
        /// <summary>
        /// 价格
        /// </summary>
        [Required]
        public double Price
        {
            get { return Retrieve(r => r.Price); }
            set { Store(r => r.Price, value); }
        }
        /// <summary>
        /// 折扣价
        /// </summary>
        public double DiscountPrice
        {
            get { return Retrieve(r => r.DiscountPrice, -1); }
            set { Store(r => r.DiscountPrice, value); }
        }
        /// <summary>
        /// 数字商品、虚拟商品(没有物流)
        /// </summary>
        public bool IsDigital
        {
            get { return Retrieve(r => r.IsDigital); }
            set { Store(r => r.IsDigital, value); }
        }

        /// <summary>
        /// 物流费用 -不填写时将使用物流费用模板进行计算
        /// </summary>
        public double? ShippingCost
        {
            get { return Retrieve(r => r.ShippingCost); }
            set { Store(r => r.ShippingCost, value); }
        }
        /// <summary>
        /// 中奖
        /// </summary>
        public double Weight
        {
            get { return Retrieve(r => r.Weight); }
            set { Store(r => r.Weight, value); }
        }
        /// <summary>
        /// 规格
        /// </summary>
        public string Size
        {
            get { return Retrieve(r => r.Size); }
            set { Store(r => r.Size, value); }
        }
        /// <summary>
        /// 库存
        /// </summary>
        public int Inventory
        {
            get { return Retrieve(r => r.Inventory); }
            set { Store(r => r.Inventory, value); }
        }
        /// <summary>
        /// 超过库存警告信息
        /// </summary>
        public string OutOfStockMessage
        {
            get { return Retrieve(r => r.OutOfStockMessage); }
            set { Store(r => r.OutOfStockMessage, value); }
        }
        /// <summary>
        /// 允许超库存购买
        /// </summary>
        public bool AllowBackOrder
        {
            get { return Retrieve(r => r.AllowBackOrder); }
            set { Store(r => r.AllowBackOrder, value); }
        }
        /// <summary>
        /// 覆盖阶梯价格,
        /// </summary>
        public bool OverrideTieredPricing
        {
            get { return Retrieve(r => r.OverrideTieredPricing); }
            set { Store(r => r.OverrideTieredPricing, value); }
        }
        /// <summary>
        /// 价格阶梯,(折扣逻辑)
        /// </summary>
        public IEnumerable<PriceTier> PriceTiers
        {
            get
            {
                var rawTiers = Retrieve<string>("PriceTiers");
                return PriceTier.DeserializePriceTiers(rawTiers);
            }
            set
            {
                var serializedTiers = PriceTier.SerializePriceTiers(value);
                Store("PriceTiers", serializedTiers ?? "");
            }
        }
        /// <summary>
        /// 最小起订数
        /// </summary>
        public int MinimumOrderQuantity
        {
            get
            {
                var minimumOrderQuantity = Retrieve(r => r.MinimumOrderQuantity);
                return minimumOrderQuantity > 1 ? minimumOrderQuantity : 1;
            }
            set
            {
                var minimumOrderQuantity = value > 1 ? value : 1;
                Store(r => r.MinimumOrderQuantity, minimumOrderQuantity);
            }
        }
        /// <summary>
        /// 是否要求必须登陆后购买
        /// </summary>
        public bool AuthenticationRequired
        {
            get { return Retrieve(r => r.AuthenticationRequired); }
            set { Store(r => r.AuthenticationRequired, value); }
        }
    }
}

  2.  套餐商品类 (目前UI菜单中不公布,此功能与流程调试中)

    /// <summary>
    /// 产品套餐
    /// </summary>
    
    public class BundlePart : ContentPart<BundlePartRecord>
    {
        public IEnumerable<int> ProductIds
        {
            get { return Record.Products.Select(p => p.ContentItemRecord.Id); }
        }

        public IEnumerable<ProductQuantity> ProductQuantities
        {
            get
            {
                return
                    Record.Products.Select(
                        p => new ProductQuantity
                        {
                            Quantity = p.Quantity,
                            ProductId = p.ContentItemRecord.Id
                        });
            }
        }
    }

  二。物流计费方案模块

说明: 该模块为商品所需运费自动匹配计算的功能, 如果商品中指定了【运费】金额,则不从此处计算运费。  

目前支持 【重量计算规则】和【大小规格计算】

有效区域应为 所有省份, 目前仅提供几个周边省份(测试数据)

 

实体的相关代码

namespace Aivics.Commerce.Models
{
    /// <summary>
    /// 基于重量的物流计费
    /// </summary>
    
    public class WeightBasedShippingMethodPart : ContentPart<WeightBasedShippingMethodPartRecord>,
        IShippingMethod
    {
        public string Name
        {
            get { return Retrieve(r => r.Name); }
            set { Store(r => r.Name, value); }
        }

        public string ShippingCompany
        {
            get { return Retrieve(r => r.ShippingCompany); }
            set { Store(r => r.ShippingCompany, value); }
        }

        public double Price
        {
            get { return Retrieve(r => r.Price); }
            set { Store(r => r.Price, value); }
        }

        public string IncludedShippingAreas
        {
            get { return Retrieve(r => r.IncludedShippingAreas); }
            set { Store(r => r.IncludedShippingAreas, value); }
        }

        public string ExcludedShippingAreas
        {
            get { return Retrieve(r => r.ExcludedShippingAreas); }
            set { Store(r => r.ExcludedShippingAreas, value); }
        }

        public double? MinimumWeight
        {
            get { return Retrieve(r => r.MinimumWeight); }
            set { Store(r => r.MinimumWeight, value); }
        }

        public double? MaximumWeight
        {
            get { return Retrieve(r => r.MaximumWeight); }
            set { Store(r => r.MaximumWeight, value); }
        } // Set to double.PositiveInfinity (the default) for unlimited weight ranges

        public IEnumerable<ShippingOption> ComputePrice(
            IEnumerable<ShoppingCartQuantityProduct> productQuantities,
            IEnumerable<IShippingMethod> shippingMethods,
            string country,
            string zipCode,
            IWorkContextAccessor workContextAccessor)
        {

            var quantities = productQuantities.ToList();
            var fixedCost = quantities
                .Where(pq => pq.Product.ShippingCost != null && pq.Product.ShippingCost >= 0 && !pq.Product.IsDigital)
                .Sum(pq => pq.Quantity * (double)pq.Product.ShippingCost);
            var weight = quantities
                .Where(pq => (pq.Product.ShippingCost == null || pq.Product.ShippingCost < 0) && !pq.Product.IsDigital)
                .Sum(pq => pq.Quantity * pq.Product.Weight);
            if (weight.CompareTo(0) == 0)
            {
                yield return GetOption(fixedCost);
            }
            else if (weight >= MinimumWeight && weight <= MaximumWeight)
            {
                yield return GetOption(fixedCost + Price);
            }
        }

        private ShippingOption GetOption(double price)
        {
            return new ShippingOption
            {
                Description = Name,
                Price = price,
                IncludedShippingAreas =
                    IncludedShippingAreas == null
                        ? new string[] { }
                        : IncludedShippingAreas.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
                ExcludedShippingAreas =
                    ExcludedShippingAreas == null
                        ? new string[] { }
                        : ExcludedShippingAreas.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
            };
        }
    }
}

三。商品阶梯价格

说明:可设置一个全局的阶梯价格表, (满立减的规则, 同时商品可以对此进行覆盖)

如果需要做定期的促销类活动,则可使用下面一个模块。。。。

  public class TieredPriceProvider : ITieredPriceProvider
    {
        private readonly IWorkContextAccessor _wca;

        public TieredPriceProvider(IWorkContextAccessor wca)
        {
            _wca = wca;
        }

        public ShoppingCartQuantityProduct GetTieredPrice(ShoppingCartQuantityProduct quantityProduct)
        {
            var priceTiers = GetPriceTiers(quantityProduct.Product);
            var priceTier = priceTiers != null ? priceTiers
                .Where(t => t.Quantity <= quantityProduct.Quantity)
                .OrderByDescending(t => t.Quantity).Take(1).SingleOrDefault() : null;
            if (priceTier != null)
            {
                quantityProduct.Price = (double)priceTier.Price;
            }
            return quantityProduct;
        }

        public IEnumerable<PriceTier> GetPriceTiers(ProductPart product)
        {
            var productSettings = _wca.GetContext().CurrentSite.As<ProductSettingsPart>();
            IEnumerable<PriceTier> priceTiers = null;
            List<PriceTier> adjustedPriceTiers = new List<PriceTier>();

            if (productSettings.AllowProductOverrides && product.OverrideTieredPricing)
            {
                priceTiers = product.PriceTiers;
            }
            else if (productSettings.DefineSiteDefaults && (!productSettings.AllowProductOverrides || !product.OverrideTieredPricing))
            {
                priceTiers = productSettings.PriceTiers;
            }

            if (priceTiers == null)
                return priceTiers;

            foreach (var tier in priceTiers)
            {
                var adjustedPrice = tier.Price;

                if (tier.Price == null && tier.PricePercent != null)
                {
                    
                    adjustedPrice = product.Price * (double)tier.PricePercent / 100;
                }

                adjustedPriceTiers.Add(new PriceTier
                {
                    Price = adjustedPrice,
                    Quantity = tier.Quantity,
                    PricePercent = tier.PricePercent
                });
            }
            return adjustedPriceTiers.OrderBy(t => t.Quantity);
        }
    }

四。促销模块

说明:目前提供一个促销模块规则, 主要为满立减活动等适用。  

1.可设置折扣的比例(9折等)和固定的折扣金额

2.指定参与的时间

3.可参加此活动的角色(高级会员)

4.有效数量区间 

5.URL匹配可以设置与指定特定的商品参加。。

 

 

主要代码如下:

目前继承自IPromotion类,之后相关其他活动促销规则,可从此直接继承。(如买10赠1活动设置等)

    public class Discount : IPromotion
    {
        private readonly IWorkContextAccessor _wca;
        private readonly IClock _clock;

        public Discount(IWorkContextAccessor wca, IClock clock)
        {
            _wca = wca;
            _clock = clock;
        }

        public DiscountPart DiscountPart { get; set; }
        public IContent ContentItem { get { return DiscountPart.ContentItem; } }
        public string Name { get { return DiscountPart == null ? "Discount" : DiscountPart.Name; } }

        public bool Applies(ShoppingCartQuantityProduct quantityProduct, IEnumerable<ShoppingCartQuantityProduct> cartProducts)
        {
            if (DiscountPart == null) return false;
            var now = _clock.UtcNow;
            if (DiscountPart.StartDate != null && DiscountPart.StartDate > now) return false;
            if (DiscountPart.EndDate != null && DiscountPart.EndDate < now) return false;
            if (DiscountPart.StartQuantity != null &&
                DiscountPart.StartQuantity > quantityProduct.Quantity)
                return false;
            if (DiscountPart.EndQuantity != null &&
                DiscountPart.EndQuantity < quantityProduct.Quantity)
                return false;
            if (!string.IsNullOrWhiteSpace(DiscountPart.Pattern) || !string.IsNullOrWhiteSpace(DiscountPart.ExclusionPattern))
            {
                string path = null;
                if (DiscountPart.DisplayUrlResolver != null)
                {
                    path = DiscountPart.DisplayUrlResolver(quantityProduct.Product);
                }
                else if (_wca.GetContext().HttpContext != null)
                {
                    var urlHelper = new UrlHelper(_wca.GetContext().HttpContext.Request.RequestContext);
                    path = urlHelper.ItemDisplayUrl(quantityProduct.Product);
                }
                else
                {
                    var autoroutePart = quantityProduct.Product.As<AutoroutePart>();
                    if (autoroutePart != null)
                    {
                        path = "/" + autoroutePart.Path; 
                    }
                }
                if (path == null) return false;
                if (!string.IsNullOrWhiteSpace(DiscountPart.Pattern))
                {
                    var patternExpression = new Regex(DiscountPart.Pattern, RegexOptions.Singleline | RegexOptions.IgnoreCase);
                    if (!patternExpression.IsMatch(path))
                        return false;
                }
                if (!string.IsNullOrWhiteSpace(DiscountPart.ExclusionPattern))
                {
                    var exclusionPatternExpression = new Regex(DiscountPart.ExclusionPattern,
                        RegexOptions.Singleline | RegexOptions.IgnoreCase);
                    if (exclusionPatternExpression.IsMatch(path))
                        return false;
                }
            }
            if (DiscountPart.Roles.Any())
            {
                var user = _wca.GetContext().CurrentUser;
                if (!user.Has<IUserRoles>()) return false;
                var roles = user.As<IUserRoles>().Roles;
                if (!roles.Any(r => DiscountPart.Roles.Contains(r))) return false;
            }

            return true;
        }

        public ShoppingCartQuantityProduct Apply(ShoppingCartQuantityProduct quantityProduct, IEnumerable<ShoppingCartQuantityProduct> cartProducts)
        {
            if (DiscountPart == null) return quantityProduct;
            var comment = DiscountPart.Comment; 
            var percent = DiscountPart.DiscountPercent;
            if (percent != null)
            {
                return new ShoppingCartQuantityProduct(quantityProduct.Quantity, quantityProduct.Product, quantityProduct.AttributeIdsToValues)
                {
                    Comment = comment,
                    Price = Math.Round(quantityProduct.Price * (1 - ((double)percent / 100)), 2),
                    Promotion = DiscountPart
                };
            }
            var discount = DiscountPart.Discount;
            if (discount != null)
            {
                return new ShoppingCartQuantityProduct(quantityProduct.Quantity, quantityProduct.Product, quantityProduct.AttributeIdsToValues)
                {
                    Comment = comment,
                    Price = Math.Round(Math.Max(0, quantityProduct.Price - (double)discount), 2),
                    Promotion = DiscountPart
                };
            }
            return quantityProduct;
        }
    }

五。商品扩展模块

主要为解决需要 用户确定附属配置的 商品 。  用户可在选择了主商品的基础上, 选择额外配置, 不同的配置将决定追加的金额不同。 

(这个功能需与购物车整合体现)

namespace Aivics.Commerce.Models
{
    /// <summary>
    /// 商品扩展插件对象  用户可选择不同的插件,需支付额外的插件价格
    /// </summary>
    
    public class ProductAttributePart : ContentPart<ProductAttributePartRecord>
    {
        public IEnumerable<ProductAttributeValue> AttributeValues
        {
            get
            {
                return ProductAttributeValue.DeserializeAttributeValues(AttributeValuesString);
            }
            set
            {
                AttributeValuesString = ProductAttributeValue.SerializeAttributeValues(value);
            }
        }
        /// <summary>
        /// 排序号
        /// </summary>
        [DisplayName("Sort Order")]
        public int SortOrder
        {
            get { return Retrieve(r => r.SortOrder); }
            set { Store(r => r.SortOrder, value); }
        }
        /// <summary>
        /// 显示名
        /// </summary>
        [DisplayName("Display Name")]
        public string DisplayName
        {
            get { return Retrieve(r => r.DisplayName); }
            set { Store(r => r.DisplayName, value); }
        }
        /// <summary>
        /// 设置信息
        /// </summary>
        internal string AttributeValuesString
        {
            get
            {
                return Retrieve(r => r.AttributeValues);
            }
            set
            {
                Store(r => r.AttributeValues, value);
            }
        }
    }
}

 

六。订单管理

查看所有用户下单等。

 

 

 

 

七。购物车模块设置

除了domain/cart为进入购物车页面外,  购物车模块已经做成widget. 可以做到layout的其中一个位置固定。

 

 购物车相关代码:

   [OrchardFeature("Aivics.Commerce")]
    public class ShoppingCart : IShoppingCart
    {
        private readonly IContentManager _contentManager;
        private readonly IShoppingCartStorage _cartStorage;
        private readonly IPriceService _priceService;
        private readonly IEnumerable<IProductAttributesDriver> _attributesDrivers;
        private readonly INotifier _notifier;

        private IEnumerable<ShoppingCartQuantityProduct> _products;

        public ShoppingCart(
            IContentManager contentManager,
            IShoppingCartStorage cartStorage,
            IPriceService priceService,
            IEnumerable<IProductAttributesDriver> attributesDrivers,
            INotifier notifier)
        {

            _contentManager = contentManager;
            _cartStorage = cartStorage;
            _priceService = priceService;
            _attributesDrivers = attributesDrivers;
            _notifier = notifier;
            T = NullLocalizer.Instance;
        }

        public Localizer T { get; set; }

        public IEnumerable<ShoppingCartItem> Items
        {
            get { return ItemsInternal.AsReadOnly(); }
        }


        private List<ShoppingCartItem> ItemsInternal
        {
            get
            {
                return _cartStorage.Retrieve();
            }
        }

        /// <summary>
        /// 添加商品至购物车中,目前将商品存放在session中
        /// </summary>
        /// <param name="productId"></param>
        /// <param name="quantity"></param>
        /// <param name="attributeIdsToValues"></param>
        public void Add(int productId, int quantity = 1, IDictionary<int, ProductAttributeValueExtended> attributeIdsToValues = null)
        {
            if (!ValidateAttributes(productId, attributeIdsToValues))
            {
                // 将该商品添加到购物车时,该商品扩展属性不正确(或后台有更新,或前台数据结构异常)。
                _notifier.Warning(T("Couldn't add this product because of invalid attributes. Please refresh the page and try again."));
                return;
            }
            var item = FindCartItem(productId, attributeIdsToValues);
            if (item != null)
            {
                item.Quantity += quantity;
            }
            else
            {
                ItemsInternal.Insert(0, new ShoppingCartItem(productId, quantity, attributeIdsToValues));
            }
            _products = null;
        }

        /// <summary>
        /// 查找一个商品, 可以通过商品id直接从查询,或者也需同时传递扩展属性进行匹配
        /// </summary>
        /// <param name="productId"></param>
        /// <param name="attributeIdsToValues"></param>
        /// <returns></returns>
        public ShoppingCartItem FindCartItem(int productId, IDictionary<int, ProductAttributeValueExtended> attributeIdsToValues = null)
        {
            if (attributeIdsToValues == null || attributeIdsToValues.Count == 0)
            {
                return Items.FirstOrDefault(i => i.ProductId == productId
                      && (i.AttributeIdsToValues == null || i.AttributeIdsToValues.Count == 0));
            }
            return Items.FirstOrDefault(
                i => i.ProductId == productId
                     && i.AttributeIdsToValues != null
                     && i.AttributeIdsToValues.Count == attributeIdsToValues.Count
                     && i.AttributeIdsToValues.All(attributeIdsToValues.Contains));
        }
        /// <summary>
        /// 验证该商品扩展属性是否正确(或后台有更新,或前台数据结构异常)。
        /// </summary>
        /// <param name="productId"></param>
        /// <param name="attributeIdsToValues"></param>
        /// <returns></returns>
        private bool ValidateAttributes(int productId, IDictionary<int, ProductAttributeValueExtended> attributeIdsToValues)
        {
            if (_attributesDrivers == null ||
                attributeIdsToValues == null ||
                !attributeIdsToValues.Any()) return true;

            var product = _contentManager.Get(productId);
            return _attributesDrivers.All(d => d.ValidateAttributes(product, attributeIdsToValues));
        }
        /// <summary>
        /// 批量添加商品至购物车
        /// </summary>
        /// <param name="items"></param>
        public void AddRange(IEnumerable<ShoppingCartItem> items)
        {
            foreach (var item in items)
            {
                Add(item.ProductId, item.Quantity, item.AttributeIdsToValues);
            }
        }
        /// <summary>
        /// 移除购物车商品
        /// </summary>
        /// <param name="productId"></param>
        /// <param name="attributeIdsToValues"></param>
        public void Remove(int productId, IDictionary<int, ProductAttributeValueExtended> attributeIdsToValues = null)
        {
            var item = FindCartItem(productId, attributeIdsToValues);
            if (item == null) return;

            ItemsInternal.Remove(item);
            _products = null;
        }
        /// <summary>
        /// 获取目前选购的商品  (1.数量>0, 2. 重新从服务器匹配商品,避免商品被删除,脏数据。
        /// </summary>
        /// <returns></returns>
        public IEnumerable<ShoppingCartQuantityProduct> GetProducts()
        {
            if (_products != null) return _products;

            //从session中获得所有保存的商品ID  (用户购物车)
            var ids = Items.Select(x => x.ProductId);


            var productParts =
                _contentManager.GetMany<ProductPart>(ids, VersionOptions.Published,
                new QueryHints().ExpandParts<TitlePart, ProductPart, AutoroutePart>()).ToArray();

            var productPartIds = productParts.Select(p => p.Id);

            //保证Session中存储的ID是 服务器端 在用的Product对象 (排除掉未发布,删除等,避免脏数据)
            var shoppingCartQuantities =
                (from item in Items
                 where productPartIds.Contains(item.ProductId) && item.Quantity > 0
                 select new ShoppingCartQuantityProduct(item.Quantity, productParts.First(p => p.Id == item.ProductId), item.AttributeIdsToValues))  //使用ShoppingCartQuantityProduct,完善的购物车商品字段等信息
                    .ToList();

            //返回所有>0选购条数的商品
            return _products = shoppingCartQuantities
                .Select(q => _priceService.GetDiscountedPrice(q, shoppingCartQuantities))
                .Where(q => q.Quantity > 0)
                .ToList();
        }

        /// <summary>
        /// 更新购物车数据,删除数量为0的数据  *疑问点*
        /// </summary>
        public void UpdateItems()
        {
            ItemsInternal.RemoveAll(x => x.Quantity <= 0);
            _products = null;
        }
        /// <summary>
        /// 获得总价格
        /// </summary>
        /// <returns></returns>
        public double Subtotal()
        {
            return Math.Round(GetProducts().Sum(pq => Math.Round(pq.Price * pq.Quantity + pq.LinePriceAdjustment, 2)), 2);
        }


        /// <summary>
        /// 总价
        /// </summary>
        /// <param name="subTotal"></param>
        /// <returns></returns>
        public double Total(double subTotal = 0)
        {
            
            if (subTotal.Equals(0))
            {
                subTotal = Subtotal();
            }
            
            return subTotal;
        }
        /// <summary>
        /// 购买总数
        /// </summary>
        /// <returns></returns>
        public double ItemCount()
        {
            return Items.Sum(x => x.Quantity);
        }
        /// <summary>
        /// 清空
        /// </summary>
        public void Clear()
        {
            _products = null;
            ItemsInternal.Clear();
            UpdateItems();
        }
    }

目前购物车使用 Session存储。

 

八。商品列表,详情

商品的列表与详情我们完全可以借助于orchard本身的模块实现,  简单介绍。。具体可查询 orchard Projection 等相关 

一。创建筛选 (可理解为创建分页查询与筛选语句等。。)

二。创建Projection或者Projection Widget

 

转载于:https://www.cnblogs.com/cuiyangMJ/p/5903875.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值