前言:使用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