在这部分,我们要完成的工作有:

1:将购物车内的商品变成真正的订单;

2:理解 父子及一对多关系;

3:写一个针对 Event Bus 的扩展点;

4:实现一个针对该扩展点的模拟的 支付服务;

一:创建订单

Views/Checkout.Summary.cshtml:

@using Orchard.ContentManagement
@using TMinji.Shop.Models
@{
    Style.Require("TMinji.Shop.Checkout.Summary");
    var shoppingCart = Model.ShoppingCart;
    var invoiceAddress = Model.InvoiceAddress;
    var shippingAddress = Model.ShippingAddress;
    var items = (IList<dynamic>)shoppingCart.ShopItems;
    var subtotal = (decimal)shoppingCart.Subtotal;
    var vat = (decimal)shoppingCart.Vat;
    var total = (decimal)shoppingCart.Total;
}
@if (!items.Any())
{
    <p>You don't have any items in your shopping cart.</p>
    <a class="button" href="#">Continue shopping</a>
}
else
{

<article class="shoppingcart">
        <h2>Review your order</h2>
        <p>Please review the information below. Hit the Place Order button to proceed.</p>
        <table>
            <thead>
                <tr>
                    <td>Article</td>
                    <td class="numeric">Unit Price</td>
                    <td class="numeric">Quantity</td>
                    <td class="numeric">Total Price</td>
                </tr>
            </thead>
            <tbody>
                @for (var i = 0; i < items.Count; i++)
                {
                    var item = items[i];
                    var product = (ProductPart)item.Product;
                    var contentItem = (ContentItem)item.ContentItem;
                    var title = item.Title;
                    var quantity = (int)item.Quantity;
                    var unitPrice = product.UnitPrice;
                    var totalPrice = quantity * unitPrice;
                    <tr>
                        <td>@title</td>
                        <td class="numeric">@unitPrice.ToString("c")</td>
                        <td class="numeric">@quantity</td>
                        <td class="numeric">@totalPrice.ToString("c")</td>
                    </tr>
                }

</tbody>
            <tfoot>
                <tr class="separator"><td colspan="4">&nbsp;</td></tr>
                <tr>
                    <td class="numeric label" colspan="2">Subtotal:</td>
                    <td class="numeric">@subtotal.ToString("c")</td>
                    <td></td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="2">VAT (19%):</td>
                    <td class="numeric">@vat.ToString("c")</td>
                    <td></td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="3">Total:</td>
                    <td class="numeric">@total.ToString("c")</td>
                    <td></td>
                </tr>
            </tfoot>
        </table>
    </article>

<article class="addresses form">
        <div class="invoice-address">
            <h2>Invoice Address</h2>
            <ul class="address-fields">
                <li>@invoiceAddress.Name.Value</li>
                <li>@invoiceAddress.AddressLine1.Value</li>
                <li>@invoiceAddress.AddressLine2.Value</li>
                <li>@invoiceAddress.Zipcode.Value</li>
                <li>@invoiceAddress.City.Value</li>
                <li>@invoiceAddress.Country.Value</li>
            </ul>
        </div>
        <div class="shipping-address">
            <h2>Shipping Address</h2>
            <ul class="address-fields">
                <li>@shippingAddress.Name.Value</li>
                <li>@shippingAddress.AddressLine1.Value</li>
                <li>@shippingAddress.AddressLine2.Value</li>
                <li>@shippingAddress.Zipcode.Value</li>
                <li>@shippingAddress.City.Value</li>
                <li>@shippingAddress.Country.Value</li>
            </ul>
        </div>
    </article>

<article>
        <div class="group">
            <div class="align left"><a href="#">Cancel</a></div>
            <div class="align right">
                @using (Html.BeginFormAntiForgeryPost(Url.Action("Create", "Order", new { area = "TMinji.Shop" })))
                {
                    <button type="submit">Place Order</button>
                }
            </div>
        </div>
    </article>
}

Controllers/OrderController.cs:

using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using Orchard.Mvc;
using Orchard.Themes;
using Orchard.Localization;
using Orchard.Security;
using TMinji.Shop.ViewModels;
using TMinji.Shop.Services;
using TMinji.Shop.Models;
using TMinji.Shop.Helpers;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;

namespace TMinji.Shop.Controllers
{
    public class OrderController : Controller
    {
        private readonly dynamic _shapeFactory;
        private readonly IOrderService _orderService;
        private readonly IAuthenticationService _authenticationService;
        private readonly IShoppingCart _shoppingCart;
        private readonly ICustomerService _customerService;
        private readonly Localizer _t;

public OrderController(
            IShapeFactory shapeFactory,
            IOrderService orderService,
            IAuthenticationService authenticationService,
            IShoppingCart shoppingCart,
            ICustomerService customerService)
        {
            _shapeFactory = shapeFactory;
            _orderService = orderService;
            _authenticationService = authenticationService;
            _shoppingCart = shoppingCart;
            _customerService = customerService;
            _t = NullLocalizer.Instance;
        }

[Themed, HttpPost]
        public ActionResult Create()
        {

var user = _authenticationService.GetAuthenticatedUser();

if (user == null)
                throw new OrchardSecurityException(_t("Login required"));

var customer = user.ContentItem.As<CustomerPart>();

if (customer == null)
                throw new InvalidOperationException("The current user is not a customer");

var order = _orderService.CreateOrder(customer.Id, _shoppingCart.Items);

// Todo: Give payment service providers a chance to process payment by sending a event. If no PSP handled the event, we'll just continue by displaying the created order.
            // Raise an OrderCreated event

// If we got here, no PSP handled the OrderCreated event, so we'll just display the order.
            var shape = _shapeFactory.Order_Created(
                Order: order,
                Products: _orderService.GetProducts(order.Details).ToArray(),
                Customer: customer,
                InvoiceAddress: (dynamic)_customerService.GetAddress(user.Id, "InvoiceAddress"),
                ShippingAddress: (dynamic)_customerService.GetAddress(user.Id, "ShippingAddress")
            );
            return new ShapeResult(this, shape);
        }
    }

}

Views/Order.Created.cshtml:

@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@using TMinji.Shop.ViewModels
@using Orchard.Core;
@{
    Style.Require("TMinji.Shop.Common");
    var order = (OrderRecord) Model.Order;
    var productParts = (IList<ProductPart>) Model.Products;
    var invoiceAddress = Model.InvoiceAddress;
    var shippingAddress = Model.ShippingAddress;

}
<h2>@T("Order {0} has been created", order.GetNumber())</h2>
<p>@T("Please find your order details below")</p>

<div class="order-wrapper">
    <article class="order">
        <header>
            <ul>
                <li>
                    <div class="field-label">Order Number</div>
                    <div class="field-value">@order.GetNumber()</div>
                </li>
                <li>
                    <div class="field-label">Created</div>
                    <div class="field-value">@order.CreatedAt.ToString(System.Globalization.CultureInfo.InvariantCulture)</div>
                </li>
            </ul>
        </header>
        <table>
            <thead>
                <tr>
                    <td>Article</td>
                    <td class="numeric">Unit Price</td>
                    <td class="numeric">Quantity</td>
                    <td class="numeric">Total Price</td>
                </tr>
            </thead>
            <tbody>
                @foreach (var detail in order.Details)
                {
                    var productPart = productParts.Single(x => x.Id == detail.ProductId);
                    var routePart = productPart.As<TitlePart>();
                    var productTitle = routePart != null ? routePart.Title : "(No RoutePart attached)";
                    <tr>
                        <td>@productTitle</td>
                        <td class="numeric">@detail.UnitPrice.ToString("c")</td>
                        <td class="numeric">@detail.Quantity</td>
                        <td class="numeric">@detail.GetSubTotal().ToString("c")</td>
                    </tr>
                }
            </tbody>
            <tfoot>
                <tr class="separator"><td colspan="4">&nbsp;</td></tr>
                <tr>
                    <td class="numeric label" colspan="2">Subtotal:</td>
                    <td class="numeric">@order.SubTotal.ToString("c")</td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="2">VAT:</td>
                    <td class="numeric">@order.Vat.ToString("c")</td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="2">Total:</td>
                    <td class="numeric">@order.GetTotal().ToString("c")</td>
                </tr>
            </tfoot>
        </table>
    </article>

<article class="addresses form">
        <div class="invoice-address">
            <h2>Invoice Address</h2>
            <ul class="address-fields">
                <li>@invoiceAddress.Name.Value</li>
                <li>@invoiceAddress.AddressLine1.Value</li>
                <li>@invoiceAddress.AddressLine2.Value</li>
                <li>@invoiceAddress.Zipcode.Value</li>
                <li>@invoiceAddress.City.Value</li>
                <li>@invoiceAddress.Country.Value</li>
            </ul>
        </div>
        <div class="shipping-address">
            <h2>Shipping Address</h2>
            <ul class="address-fields">
                <li>@shippingAddress.Name.Value</li>
                <li>@shippingAddress.AddressLine1.Value</li>
                <li>@shippingAddress.AddressLine2.Value</li>
                <li>@shippingAddress.Zipcode.Value</li>
                <li>@shippingAddress.City.Value</li>
                <li>@shippingAddress.Country.Value</li>
            </ul>
        </div>
    </article>
</div>

Models/OrderDetailRecord.cs:

using Orchard.ContentManagement.Records;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TMinji.Shop.Models
{
    public class OrderDetailRecord
    {
        public virtual int Id { get; set; }
        public virtual int OrderRecord_Id { get; set; }
        public virtual int ProductId { get; set; }
        public virtual int Quantity { get; set; }
        public virtual decimal UnitPrice { get; set; }
        public virtual decimal VatRate { get; set; }

//private decimal unitVat;
        //public virtual decimal UnitVat
        //{
        //    get { return UnitPrice * VatRate; }
        //    set { unitVat = value; }
        //}
        public virtual decimal GetUnitVat()
        {
            return UnitPrice * VatRate;
        }

//private decimal vat;
        //public virtual decimal Vat
        //{
        //    get { return UnitVat * Quantity; }
        //    set { vat = value; }
        //}
        public virtual decimal GetVat()
        {
            return GetUnitVat() * Quantity;
        }

//private decimal subTotal;
        //public virtual decimal SubTotal
        //{
        //    get { return UnitPrice * Quantity; }
        //    set { subTotal = value; }
        //}
        public virtual decimal GetSubTotal()
        {
            return UnitPrice * Quantity;
        }

//private decimal total;
        //public virtual decimal Total
        //{
        //    get { return SubTotal + Vat; }
        //    set { total = value; }
        //}
        public virtual decimal GetTotal()
        {
            return GetSubTotal() + GetVat();
        }
    }

}

Models/OrderRecord.cs:

using Orchard.ContentManagement.Records;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TMinji.Shop.Models
{
    public class OrderRecord
    {
        public virtual int Id { get; set; }
        public virtual int CustomerId { get; set; }
        public virtual DateTime CreatedAt { get; set; }
        public virtual decimal SubTotal { get; set; }
        public virtual decimal Vat { get; set; }
        public virtual OrderStatus Status { get; set; }
        public virtual IList<OrderDetailRecord> Details { get; private set; }
        public virtual string PaymentServiceProviderResponse { get; set; }
        public virtual string PaymentReference { get; set; }
        public virtual DateTime? PaidAt { get; set; }
        public virtual DateTime? CompletedAt { get; set; }
        public virtual DateTime? CancelledAt { get; set; }

////private decimal total;
        //public virtual decimal Total
        //{
        //    get { return SubTotal + Vat; }
        //    //private set { total = value; }
        //}

public virtual decimal GetTotal()
        {
            return SubTotal + Vat;
        }

////private string number;
        //public virtual string Number
        //{
        //    get { return (Id + 1000).ToString(CultureInfo.InvariantCulture); }
        //    //private set { number = value; }
        //}
        public virtual string GetNumber()
        {
            return (Id + 1000).ToString(CultureInfo.InvariantCulture);
        }

public OrderRecord()
        {
            Details = new List<OrderDetailRecord>();
        }

public virtual void UpdateTotals()
        {
            var subTotal = 0m;
            var vat = 0m;

foreach (var detail in Details)
            {
                subTotal += detail.GetSubTotal();
                vat += detail.GetVat();
            }

SubTotal = subTotal;
            Vat = vat;
        }
    }

}

Models/OrderStatus.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TMinji.Shop.Models
{
    public enum OrderStatus
    {
        /// <summary>
        /// The order is new and is yet to be paid for
        /// </summary>
        New,

/// <summary>
        /// The order has been paid for, so it's eligable for shipping
        /// </summary>
        Paid,

/// <summary>
        /// The order has shipped
        /// </summary>
        Completed,

/// <summary>
        /// The order was cancelled
        /// </summary>
        Cancelled
    }

}

Migrations.cs:

using Orchard.ContentManagement.MetaData;
using Orchard.Core.Common.Fields;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;
using Orchard.Users.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Models;

namespace TMinji.Shop
{
    public class Migrations : DataMigrationImpl
    {
        public int Create()
        {

SchemaBuilder.CreateTable("ProductPartRecord", table => table
                .ContentPartRecord()
                .Column<decimal>("UnitPrice")
                .Column<string>("Sku", column => column.WithLength(50))
                );

return 1;
        }

public int UpdateFrom1()
        {
            ContentDefinitionManager.AlterPartDefinition("ProductPart", part => part
                .Attachable());

return 2;
        }

public int UpdateFrom2()
        {
            // Define a new content type called "ShoppingCartWidget"
            ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
                // Attach the "ShoppingCartWidgetPart"
                .WithPart("ShoppingCartWidgetPart")
                // In order to turn this content type into a widget, it needs the WidgetPart
                .WithPart("WidgetPart")
                // It also needs a setting called "Stereotype" to be set to "Widget"
                .WithSetting("Stereotype", "Widget")
                );

return 3;
        }

public int UpdateFrom3()
        {
            // Update the ShoppingCartWidget so that it has a CommonPart attached, which is required for widgets (it's generally a good idea to have this part attached)
            ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
                .WithPart("CommonPart")
            );

return 4;
        }

public int UpdateFrom4()
        {
            SchemaBuilder.CreateTable("CustomerPartRecord", table => table
                .ContentPartRecord()
                .Column<string>("FirstName", c => c.WithLength(50))
                .Column<string>("LastName", c => c.WithLength(50))
                .Column<string>("Title", c => c.WithLength(10))
                .Column<DateTime>("CreatedUtc")
                );

SchemaBuilder.CreateTable("AddressPartRecord", table => table
                .ContentPartRecord()
                .Column<int>("CustomerId")
                .Column<string>("Type", c => c.WithLength(50))
                );

ContentDefinitionManager.AlterPartDefinition("CustomerPart", part => part
                .Attachable(false)
                );

ContentDefinitionManager.AlterTypeDefinition("Customer", type => type
                .WithPart("CustomerPart")
                .WithPart("UserPart")
                );

ContentDefinitionManager.AlterPartDefinition("AddressPart", part => part
                .Attachable(false)
                .WithField("Name", f => f.OfType("TextField"))
                .WithField("AddressLine1", f => f.OfType("TextField"))
                .WithField("AddressLine2", f => f.OfType("TextField"))
                .WithField("Zipcode", f => f.OfType("TextField"))
                .WithField("City", f => f.OfType("TextField"))
                .WithField("Country", f => f.OfType("TextField"))
                );

ContentDefinitionManager.AlterTypeDefinition("Address", type => type
                .WithPart("CommonPart")
                .WithPart("AddressPart")
                );

return 5;
        }

public int UpdateFrom5()
        {
            ContentDefinitionManager.AlterPartDefinition(typeof(CustomerPart).Name, p => p
                .Attachable(false)
                .WithField("Phone", f => f.OfType(typeof(TextField).Name))
                );

ContentDefinitionManager.AlterTypeDefinition("Customer", t => t
                .WithPart(typeof(CustomerPart).Name)
                .WithPart(typeof(UserPart).Name)
                );

ContentDefinitionManager.AlterPartDefinition(typeof(AddressPart).Name, p => p
                .Attachable(false)
                .WithField("Name", f => f.OfType(typeof(TextField).Name))
                .WithField("AddressLine1", f => f.OfType(typeof(TextField).Name))
                .WithField("AddressLine2", f => f.OfType(typeof(TextField).Name))
                .WithField("Zipcode", f => f.OfType(typeof(TextField).Name))
                .WithField("City", f => f.OfType(typeof(TextField).Name))
                .WithField("Country", f => f.OfType(typeof(TextField).Name))
                );

ContentDefinitionManager.AlterTypeDefinition("Address", t => t
                .WithPart(typeof(AddressPart).Name)
                );

return 6;
        }

public int UpdateFrom6()
        {
            //FOREIGN KEY 约束"Order_Customer"冲突。表"dbo.TMinji_Shop_CustomerRecord", column 'Id'。
            SchemaBuilder.CreateTable("OrderRecord", t => t
                .Column<int>("Id", c => c.PrimaryKey().Identity())
                .Column<int>("CustomerId", c => c.NotNull())
                .Column<DateTime>("CreatedAt", c => c.NotNull())
                .Column<decimal>("SubTotal", c => c.NotNull())
                .Column<decimal>("Vat", c => c.NotNull())
                .Column<string>("Status", c => c.WithLength(50).NotNull())
                .Column<string>("PaymentServiceProviderResponse", c => c.WithLength(null))
                .Column<string>("PaymentReference", c => c.WithLength(50))
                .Column<DateTime>("PaidAt", c => c.Nullable())
                .Column<DateTime>("CompletedAt", c => c.Nullable())
                .Column<DateTime>("CancelledAt", c => c.Nullable())
                );

SchemaBuilder.CreateTable("OrderDetailRecord", t => t
                .Column<int>("Id", c => c.PrimaryKey().Identity())
                .Column<int>("OrderRecord_Id", c => c.NotNull())
                .Column<int>("ProductId", c => c.NotNull())
                .Column<int>("Quantity", c => c.NotNull())
                .Column<decimal>("UnitPrice", c => c.NotNull())
                .Column<decimal>("VatRate", c => c.NotNull())
                );

SchemaBuilder.CreateForeignKey("Order_Customer", "OrderRecord", new[] { "CustomerId" }, "CustomerPartRecord", new[] { "Id" });
            SchemaBuilder.CreateForeignKey("OrderDetail_Order", "OrderDetailRecord", new[] { "OrderRecord_Id" }, "OrderRecord", new[] { "Id" });
            SchemaBuilder.CreateForeignKey("OrderDetail_Product", "OrderDetailRecord", new[] { "ProductId" }, "ProductPartRecord", new[] { "Id" });

return 7;
        }

}

}

Services/IOrderService.cs:

using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Models;

namespace TMinji.Shop.Services
{
    public interface IOrderService : IDependency
    {
        /// <summary>
        /// Creates a new order based on the specified ShoppingCartItems
        /// </summary>
        OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);

/// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails);
    }

}

Services/OrderService.cs:

using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Models;

namespace TMinji.Shop.Services
{
    public class OrderService : IOrderService
    {
        private readonly IDateTimeService _dateTimeService;
        private readonly IRepository<ProductPartRecord> _productRepository;
        private readonly IContentManager _contentManager;
        private readonly IRepository<OrderRecord> _orderRepository;
        private readonly IRepository<OrderDetailRecord> _orderDetailRepository;
        private readonly IOrchardServices _orchardServices;

public OrderService(
            IDateTimeService dateTimeService,
            IRepository<ProductPartRecord> productRepository,
            IContentManager contentManager,
            IRepository<OrderRecord> orderRepository,
            IRepository<OrderDetailRecord> orderDetailRepository,
            IOrchardServices orchardServices)
        {
            _dateTimeService = dateTimeService;
            _productRepository = productRepository;
            _contentManager = contentManager;
            _orderRepository = orderRepository;
            _orderDetailRepository = orderDetailRepository;
            _orchardServices = orchardServices;
        }

public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items)
        {

if (items == null)
                throw new ArgumentNullException("items");

// Convert to an array to avoid re-running the enumerable
            var itemsArray = items.ToArray();

if (!itemsArray.Any())
                throw new ArgumentException("Creating an order with 0 items is not supported", "items");

var order = new OrderRecord
            {
                CreatedAt = _dateTimeService.Now,
                CustomerId = customerId,
                Status = OrderStatus.New
            };

_orderRepository.Create(order);
            // Get all products in one shot, so we can add the product reference to each order detail
            var productIds = itemsArray.Select(x => x.ProductId).ToArray();
            var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();

// Create an order detail for each item
            foreach (var item in itemsArray)
            {
                var product = products.Single(x => x.Id == item.ProductId);

var detail = new OrderDetailRecord
                {
                    OrderRecord_Id = order.Id,
                    ProductId = product.Id,
                    Quantity = item.Quantity,
                    UnitPrice = product.UnitPrice,
                    VatRate = .19m
                };

_orderDetailRepository.Create(detail);
                order.Details.Add(detail);
            }

order.UpdateTotals();

return order;
        }

/// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails)
        {
            var productIds = orderDetails.Select(x => x.ProductId).ToArray();
            return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty);
        }
    }

}

ResourceManifest.cs:

using Orchard.UI.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TMinji.Shop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            // Create and add a new manifest
            var manifest = builder.Add();

// Define a "common" style sheet
            manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");

// Define the "shoppingcart" style sheet
            manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");

manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");

//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
            // Define the "shoppingcart" script and set a dependency on the "jQuery" resource
            //manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
            manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");

manifest.DefineStyle("TMinji.Shop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("TMinji.Shop.Common");

manifest.DefineStyle("TMinji.Shop.Order").SetUrl("order.css").SetDependencies("TMinji.Shop.Common");

}
    }

}

最终结果:

数据库记录为:

二:支付之 Event Bus

Event Bus 这个机制可被用于扩展 Orchard 模块。首先,让我们看看如果 Event Bus 应用到支付中的话,其机制是怎么样的:

首先,我们要定义一个 PaymentRequest,它包含了两个属性:Created Order 和 flag,这能告诉 Event Listener 我们需要开始支付流程,我们还会定义 PaymentResponse,它包含了 payment service provider 的反馈。现在,看代码吧:

TMinji.Shop.Extensibility.PaymentRequest

public class PaymentRequest
{
    public OrderRecord Order { get; private set; }
    public bool WillHandlePayment { get; set; }
    public ActionResult ActionResult { get; set; }

public PaymentRequest(OrderRecord order)
    {
        Order = order;
    }
}

TMinji.Shop.Extensibility.PaymentResponse

public class PaymentResponse
{
    public bool WillHandleResponse { get; set; }
    public PaymentResponseStatus Status { get; set; }
    public string OrderReference { get; set; }
    public string PaymentReference { get; set; }
    public string ResponseText { get; set; }
    public HttpContextBase HttpContext { get; private set; }

public PaymentResponse(HttpContextBase httpContext)
    {
        HttpContext = httpContext;
    }
}

TMinji.Shop.Extensibility.PaymentResponseStatus

public enum PaymentResponseStatus
{
    Success,
    Failed,
    Cancelled,
    Exception
}

Extensibility/IPaymentServiceProvider.cs:

public interface IPaymentServiceProvider : IEventHandler
{
    void RequestPayment(PaymentRequest e);
    void ProcessResponse(PaymentResponse e);
}

Controllers/OrderController.cs:

private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders;
private readonly Localizer _t;

public OrderController(
    IShapeFactory shapeFactory,
    IOrderService orderService,
    IAuthenticationService authenticationService,
    IShoppingCart shoppingCart,
    ICustomerService customerService,
    IEnumerable<IPaymentServiceProvider> paymentServiceProviders)
{
    _shapeFactory = shapeFactory;
    _orderService = orderService;
    _authenticationService = authenticationService;
    _shoppingCart = shoppingCart;
    _customerService = customerService;
    _paymentServiceProviders = paymentServiceProviders;
    //_paymentServiceProvider = new SimulatedPaymentServiceProvider();
    _t = NullLocalizer.Instance;
}

这里,需要特别说明哦:

只要模块中存在 IPaymentServiceProvider 的实现类,注入机制就都会注入进这个列表,这样一来,就实现了 Event Bus

Module.txt:

name: tminji.shop
antiforgery: enabled
author: tminji.com
website: http://www.tminji.com
version: 1.0.0
orchardversion: 1.0.0
description: The tminji.com module is a shopping module.
Dependencies: Orchard.Projections, Orchard.Forms, Orchard.jQuery, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout, Orchard.Users
features:
    shop:
        Description: shopping module.
        Category: ASample
    SimulatedPSP:
        Description: Provides a simulated Payment Service Provider for testing purposes only.
        Category: ASample

然后,到后台启动我们的支付模块:

Services/SimulatedPaymentServiceProvider.cs:

using Orchard.Environment.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using TMinji.Shop.Extensibility;

namespace TMinji.Shop.Services
{
    [OrchardFeature("TMinji.Shop.SimulatedPSP")]
    public class SimulatedPaymentServiceProvider : IPaymentServiceProvider
    {
        public void RequestPayment(PaymentRequest e)
        {

e.ActionResult = new RedirectToRouteResult(new RouteValueDictionary {
                {"action", "Index"},
                {"controller", "SimulatedPaymentServiceProvider"},
                {"area", "TMinji.Shop"},
                {"orderReference", e.Order.GetNumber()},
                {"amount", (int)(e.Order.GetTotal() * 100)}
            });

e.WillHandlePayment = true;
        }

public void ProcessResponse(PaymentResponse e)
        {
            var result = e.HttpContext.Request.QueryString["result"];

e.OrderReference = e.HttpContext.Request.QueryString["orderReference"];
            e.PaymentReference = e.HttpContext.Request.QueryString["paymentId"];
            e.ResponseText = e.HttpContext.Request.QueryString.ToString();

switch (result)
            {
                case "Success":
                    e.Status = PaymentResponseStatus.Success;
                    break;
                case "Failure":
                    e.Status = PaymentResponseStatus.Failed;
                    break;
                case "Cancelled":
                    e.Status = PaymentResponseStatus.Cancelled;
                    break;
                default:
                    e.Status = PaymentResponseStatus.Exception;
                    break;
            }

e.WillHandleResponse = true;
        }
    }

}

Views/SimulatedPaymentServiceProvider/Index.cshtml:

@{
    var orderReference = (string)Model.OrderReference;
    var amount = (decimal)((int)Model.Amount) / 100;
    var commands = new[] { "Success", "Failure", "Cancelled", "Exception" };

Style.Require("TMinji.Shop.SimulatedPSP");
}

<h2>Payment Service Provider Simulation</h2>
<p>
    Received a payment request with order reference <strong>@orderReference</strong><br />
    Amount: <strong>@amount.ToString("c")</strong>
</p>
@using (Html.BeginFormAntiForgeryPost(Url.Action("Command", "SimulatedPaymentServiceProvider", new { area = "TMinji.Shop" })))
{
    <article class="form">
        <input type="hidden" name="orderReference" value="@orderReference" />
        <ul class="commands">
            @foreach (var command in commands)
            {
                <li><button type="submit" name="command" value="@command">@command</button></li>
            }
        </ul>
    </article>
}

ResourceManifest.cs:

using Orchard.UI.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TMinji.Shop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            // Create and add a new manifest
            var manifest = builder.Add();

// Define a "common" style sheet
            manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");

// Define the "shoppingcart" style sheet
            manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");

manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");

//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
            // Define the "shoppingcart" script and set a dependency on the "jQuery" resource
            //manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
            manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");

manifest.DefineStyle("TMinji.Shop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("TMinji.Shop.Common");

manifest.DefineStyle("TMinji.Shop.Order").SetUrl("order.css").SetDependencies("TMinji.Shop.Common");

manifest.DefineStyle("TMinji.Shop.SimulatedPSP").SetUrl("simulated-psp.css").SetDependencies("TMinji.Shop.Common");

}
    }

}

Controllers/SimulatedPaymentServiceProviderController.cs:

using Orchard.DisplayManagement;
using Orchard.Themes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace TMinji.Shop.Controllers
{
    public class SimulatedPaymentServiceProviderController : Controller
    {

private readonly dynamic _shapeFactory;

public SimulatedPaymentServiceProviderController(IShapeFactory shapeFactory)
        {
            _shapeFactory = shapeFactory;
        }

[Themed]
        public ActionResult Index(string orderReference, int amount)
        {
            var model = _shapeFactory.PaymentRequest(
                OrderReference: orderReference,
                Amount: amount
                );

return View(model);
        }

[HttpPost]
        public ActionResult Command(string command, string orderReference)
        {

// Generate a fake payment ID
            var paymentId = new Random(Guid.NewGuid().GetHashCode()).Next(1000, 9999);

// Redirect back to the webshop
            return RedirectToAction("PaymentResponse", "Order", new { area = "TMinji.Shop", paymentId = paymentId, result = command, orderReference });
        }
    }
}

Controllers/OrderController.cs:

using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using Orchard.Mvc;
using Orchard.Themes;
using Orchard.Localization;
using Orchard.Security;
using TMinji.Shop.ViewModels;
using TMinji.Shop.Services;
using TMinji.Shop.Models;
using TMinji.Shop.Helpers;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using TMinji.Shop.Extensibility;

namespace TMinji.Shop.Controllers
{
    public class OrderController : Controller
    {
        private readonly dynamic _shapeFactory;
        private readonly IOrderService _orderService;
        private readonly IAuthenticationService _authenticationService;
        private readonly IShoppingCart _shoppingCart;
        private readonly ICustomerService _customerService;
        private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders;
        private readonly Localizer _t;

public OrderController(
            IShapeFactory shapeFactory,
            IOrderService orderService,
            IAuthenticationService authenticationService,
            IShoppingCart shoppingCart,
            ICustomerService customerService,
            IEnumerable<IPaymentServiceProvider> paymentServiceProviders)
        {
            _shapeFactory = shapeFactory;
            _orderService = orderService;
            _authenticationService = authenticationService;
            _shoppingCart = shoppingCart;
            _customerService = customerService;
            _paymentServiceProviders = paymentServiceProviders;
            //_paymentServiceProvider = new SimulatedPaymentServiceProvider();
            _t = NullLocalizer.Instance;
        }

[Themed, HttpPost]
        public ActionResult Create()
        {

var user = _authenticationService.GetAuthenticatedUser();

if (user == null)
                throw new OrchardSecurityException(_t("Login required"));

var customer = user.ContentItem.As<CustomerPart>();

if (customer == null)
                throw new InvalidOperationException("The current user is not a customer");

var order = _orderService.CreateOrder(customer.Id, _shoppingCart.Items);

// Fire the PaymentRequest event
            var paymentRequest = new PaymentRequest(order);

foreach (var handler in _paymentServiceProviders)
            {
                handler.RequestPayment(paymentRequest);

// If the handler responded, it will set the action result
                if (paymentRequest.WillHandlePayment)
                {
                    return paymentRequest.ActionResult;
                }
            }

// If we got here, no PSP handled the OrderCreated event, so we'll just display the order.
            var shape = _shapeFactory.Order_Created(
                Order: order,
                Products: _orderService.GetProducts(order.Details).ToArray(),
                Customer: customer,
                InvoiceAddress: (dynamic)_customerService.GetAddress(user.Id, "InvoiceAddress"),
                ShippingAddress: (dynamic)_customerService.GetAddress(user.Id, "ShippingAddress")
            );
            return new ShapeResult(this, shape);
        }

[Themed]
        public ActionResult PaymentResponse()
        {

var args = new PaymentResponse(HttpContext);

foreach (var handler in _paymentServiceProviders)
            {
                handler.ProcessResponse(args);

if (args.WillHandleResponse)
                    break;
            }

if (!args.WillHandleResponse)
                throw new OrchardException(_t("Such things mean trouble"));

var order = _orderService.GetOrderByNumber(args.OrderReference);
            _orderService.UpdateOrderStatus(order, args);

if (order.Status == OrderStatus.Paid)
            {
                // Send some notification mail message to the customer that the order was paid.
                // We may also initiate the shipping process from here
            }

return new ShapeResult(this, _shapeFactory.Order_PaymentResponse(Order: order, PaymentResponse: args));
        }
    }

}

Services/IOrderService.cs:

using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Extensibility;
using TMinji.Shop.Models;

namespace TMinji.Shop.Services
{
    public interface IOrderService : IDependency
    {
        /// <summary>
        /// Creates a new order based on the specified ShoppingCartItems
        /// </summary>
        OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);

/// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails);

OrderRecord GetOrderByNumber(string orderNumber);

void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse);
    }

}

Services/OrderService.cs:

using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Extensibility;
using TMinji.Shop.Models;

namespace TMinji.Shop.Services
{
    public class OrderService : IOrderService
    {
        private readonly IDateTimeService _dateTimeService;
        private readonly IRepository<ProductPartRecord> _productRepository;
        private readonly IContentManager _contentManager;
        private readonly IRepository<OrderRecord> _orderRepository;
        private readonly IRepository<OrderDetailRecord> _orderDetailRepository;
        private readonly IOrchardServices _orchardServices;

public OrderService(
            IDateTimeService dateTimeService,
            IRepository<ProductPartRecord> productRepository,
            IContentManager contentManager,
            IRepository<OrderRecord> orderRepository,
            IRepository<OrderDetailRecord> orderDetailRepository,
            IOrchardServices orchardServices)
        {
            _dateTimeService = dateTimeService;
            _productRepository = productRepository;
            _contentManager = contentManager;
            _orderRepository = orderRepository;
            _orderDetailRepository = orderDetailRepository;
            _orchardServices = orchardServices;
        }

public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items)
        {

if (items == null)
                throw new ArgumentNullException("items");

// Convert to an array to avoid re-running the enumerable
            var itemsArray = items.ToArray();

if (!itemsArray.Any())
                throw new ArgumentException("Creating an order with 0 items is not supported", "items");

var order = new OrderRecord
            {
                CreatedAt = _dateTimeService.Now,
                CustomerId = customerId,
                Status = OrderStatus.New
            };

_orderRepository.Create(order);
            // Get all products in one shot, so we can add the product reference to each order detail
            var productIds = itemsArray.Select(x => x.ProductId).ToArray();
            var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();

// Create an order detail for each item
            foreach (var item in itemsArray)
            {
                var product = products.Single(x => x.Id == item.ProductId);

var detail = new OrderDetailRecord
                {
                    OrderRecord_Id = order.Id,
                    ProductId = product.Id,
                    Quantity = item.Quantity,
                    UnitPrice = product.UnitPrice,
                    VatRate = .19m
                };

_orderDetailRepository.Create(detail);
                order.Details.Add(detail);
            }

order.UpdateTotals();

return order;
        }

/// <summary>
        /// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
        /// </summary>
        public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails)
        {
            var productIds = orderDetails.Select(x => x.ProductId).ToArray();
            return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty);
        }

public OrderRecord GetOrderByNumber(string orderNumber)
        {
            var orderId = int.Parse(orderNumber) - 1000;
            return _orderRepository.Get(orderId);
        }

public void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse)
        {
            OrderStatus orderStatus;

switch (paymentResponse.Status)
            {
                case PaymentResponseStatus.Success:
                    orderStatus = OrderStatus.Paid;
                    break;
                default:
                    orderStatus = OrderStatus.Cancelled;
                    break;
            }

if (order.Status == orderStatus)
                return;

order.Status = orderStatus;
            order.PaymentServiceProviderResponse = paymentResponse.ResponseText;
            order.PaymentReference = paymentResponse.PaymentReference;

switch (order.Status)
            {
                case OrderStatus.Paid:
                    order.PaidAt = _dateTimeService.Now;
                    break;
                case OrderStatus.Completed:
                    order.CompletedAt = _dateTimeService.Now;
                    break;
                case OrderStatus.Cancelled:
                    order.CancelledAt = _dateTimeService.Now;
                    break;
            }
        }

}

}

Views/Order.PaymentResponse.cshtml:

@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@using TMinji.Shop.Extensibility
@using Orchard.Core;
@{
    var order = (OrderRecord)Model.Order;
    var paymentResponse = (PaymentResponse)Model.PaymentResponse;
}
@if (paymentResponse.Status == PaymentResponseStatus.Success)
{
    <h2>@T("Payment was succesful")</h2>
    <p>Thanks! We succesfully received payment for order @order.GetNumber() with payment ID @paymentResponse.PaymentReference</p>
    <p>Enjoy your products and come again!</p>
}
else
{
    <h2>@T("Order cancelled")</h2>
    <p>Your order (@order.GetNumber()) has been cancelled</p>
}

最终结果如下:

数据库结果:

Orchard模块开发全接触7:订单与支付之Event Bus的更多相关文章

  1. Orchard模块开发全接触1:起步

    在<http://www.cnblogs.com/luminji/p/3831281.html>中简单介绍了 Orchard 的模块开发,接下来,我们需要做个更复杂的例子,Orchard ...

  2. Orchard模块开发全接触8:改造后台

    后台默认提供了 Content 的管理,但是,所有的内容类型揉杂在一起,而我们需要更深度的定制一些功能,比如,我们只想管理订单,又比如,我们需要对注册用户进行管理.本篇的内容包括: 1:自定义 adm ...

  3. Orchard模块开发全接触5:深度改造前台第二部分

    在这一部分,我们继续完善我们的购物车,我们要做以下一些事情: 1:完成 shoppingcart.cshtml: 2:让用户可以更新数量及从购物车删除商品: 3:创建一个 widget,在上面可以看到 ...

  4. Orchard模块开发全接触4:深度改造前台

    这里,我们需要做一些事情,这些事情意味着深度改造前台: 1:为商品增加 添加到购物车 按钮,点击后功能实现: 2:商品排序: 3:购物车预览,以及添加 结算 按钮: 4:一个显式 购物车中有*个 商品 ...

  5. Orchard模块开发全接触3:分类的实现及内容呈现(Display)

    一:分类用现有技术怎么实现? 实际就是创建 Query 和 Projection,如果不知道怎么做,参考:Orchard之在前台显式一个属于自己的列表(在这篇里,还进行了稍稍拓展),当然,基础的知道, ...

  6. Orchard模块开发全接触2:新建 ProductPart

    一:创建 Part 1:项目引用 Orchard.Framework: 2:创建 Models 文件夹: 3:在 Models 文件夹下创建类 ProductPartRecord,如下: public ...

  7. Orchard模块开发全接触6:自定义用户注册

    我们都知道 Orchard 的用户注册相当简单,现在,我们需要一个自定义的用户注册,现在,开始吧. 一:定义实体 Models/CustomerPartRecord.cs: public class ...

  8. Orchard 模块开发学习笔记 (1)

    创建模块 首先,打开Bin目录下的Orchard.exe 等到出现orchard>后, 看看命令列表中是否存在 codegen module 如果不存在,则需要先执行:feature enabl ...

  9. Odoo9.0模块开发全流程

    构建Odoo模块 模块组成 业务对象 业务对象声明为Python类, 由Odoo自己主动加载. 数据文件 XML或CSV文件格式, 在当中声明了元数据(视图或工作流).配置数据(模块參数).演示数据等 ...

随机推荐

  1. kgtemp文件转mp3工具

    kgtemp文件是酷我音乐软件的缓存文件,本文从技术层面探讨如何解密该文件为mp3文件,并通过读取ID3信息来重命名. kgtemp解密 kgtemp文件前1024个字节是固定的包头信息,解密方案详细 ...

  2. ubuntu14.0安装ITK的步骤

    (1) sudo apt-get install cmake (2) sudo apt-get install cmake-curses-gui (3)下载安装包InsightToolkit-4.11 ...

  3. Google浏览器被360劫持

    最新更新!!! 新写的一个 Chrome浏览器被hao123劫持:    http://www.cnblogs.com/tanrong/p/7815494.html 这两个博客结合着看吧,它们几乎包括 ...

  4. MAC配置DNS服务器

    1.brew install dnsmasq 2.cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf ...

  5. MVC+easyui,写个树

    前言:网上关于编写组织机构树的教程并不少,我第一次写树的时候也是在网上借鉴别人的技术,走了一些弯路写下了树.是因为这些教程都不是很全面,对于编程新手来说跳跃性太强.所以趁着闲暇时期,我用心的写个树,供 ...

  6. web理论知识--HTML结构及标签

    一.参考书籍: <Web 前端开发 HTML5+CSS3+jQuery+AJAX 从学到用完美实践> 备注:本书为工具书. 二.HTML5元素: 按功能划分:基础.格式.表单.框架.图像. ...

  7. hdu 4605 树状数组 ****

    题目大意很简单. 有一颗树(10^5结点),所有结点要么没有子结点,要么有两个子结点.然后每个结点都有一个重量值,根结点是1 然后有一个球,从结点1开始往子孙结点走. 每碰到一个结点,有三种情况 如果 ...

  8. Code Forces 698A Vacations

    题目描述 Vasya has nn days of vacations! So he decided to improve his IT skills and do sport. Vasya know ...

  9. CSS选择符、属性继承、优先级算法以及CSS3新增伪类、新特性

    CSS 选择符有哪些?哪些属性可以继承?优先级算法如何计算? CSS3新增伪类有那些?CSS新增了哪些特性?下面我整理了一些,仅供参考. CSS 选择符: 1)      id选择器(# myid) ...

  10. 【Java】须要配置的三个Java环境变量

    我的电脑→属性→高级系统设置→高级→环境变量 1.JAVA_HOME : JDK的安装路径 2.PATH : %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; 3.CLASSP ...