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


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

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

            }
        }

    }

    最终结果:

    image

    数据库记录为:

    image

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

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

    image

    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>
    }

    最终结果如下:

    image

    image

    数据库结果:

    image

  • 相关阅读:
    产品经理经常犯的错误李可按
    skills_hive
    skills_office
    skills_idea
    skills_linux
    skills_ubuntu
    skills_git
    skills_redis
    skills_centos
    problems_hive
  • 原文地址:https://www.cnblogs.com/luminji/p/3862704.html
Copyright © 2020-2023  润新知