前言:使用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{returnRecord.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 rangespublic 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 Itemswhere productPartIds.Contains(item.ProductId) && item.Quantity > 0select 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