Orchard模块开发全接触7:订单与支付之Event Bus

时间:2022-03-06 03:26:06

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

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");

}
    }

}

最终结果:

Orchard模块开发全接触7:订单与支付之Event Bus

数据库记录为:

Orchard模块开发全接触7:订单与支付之Event Bus

二:支付之 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

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

Orchard模块开发全接触7:订单与支付之Event Bus

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

Orchard模块开发全接触7:订单与支付之Event Bus

数据库结果:

Orchard模块开发全接触7:订单与支付之Event Bus