本文共 37311 字,大约阅读时间需要 124 分钟。
在这部分,我们要完成的工作有:
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"> </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"> </td></tr> <tr> <td class="numeric label" colspan="2">Subtotal:</td> <td class="numeric">@order.SubTotal.ToString("c")</td> </tr> <tr> <td class="numeric label" colspan="2">VAT:</td> <td class="numeric">@order.Vat.ToString("c")</td> </tr> <tr> <td class="numeric label" colspan="2">Total:</td> <td class="numeric">@order.GetTotal().ToString("c")</td> </tr> </tfoot> </table> </article><article class="addresses form">
<div class="invoice-address"> <h2>Invoice Address</h2> <ul class="address-fields"> <li>@invoiceAddress.Name.Value</li> <li>@invoiceAddress.AddressLine1.Value</li> <li>@invoiceAddress.AddressLine2.Value</li> <li>@invoiceAddress.Zipcode.Value</li> <li>@invoiceAddress.City.Value</li> <li>@invoiceAddress.Country.Value</li> </ul> </div> <div class="shipping-address"> <h2>Shipping Address</h2> <ul class="address-fields"> <li>@shippingAddress.Name.Value</li> <li>@shippingAddress.AddressLine1.Value</li> <li>@shippingAddress.AddressLine2.Value</li> <li>@shippingAddress.Zipcode.Value</li> <li>@shippingAddress.City.Value</li> <li>@shippingAddress.Country.Value</li> </ul> </div> </article> </div>
Models/OrderDetailRecord.cs:
using Orchard.ContentManagement.Records;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace TMinji.Shop.Models
{ public class OrderDetailRecord { public virtual int Id { get; set; } public virtual int OrderRecord_Id { get; set; } public virtual int ProductId { get; set; } public virtual int Quantity { get; set; } public virtual decimal UnitPrice { get; set; } public virtual decimal VatRate { get; set; }//private decimal unitVat;
//public virtual decimal UnitVat //{ // get { return UnitPrice * VatRate; } // set { unitVat = value; } //} public virtual decimal GetUnitVat() { return UnitPrice * VatRate; }//private decimal vat;
//public virtual decimal Vat //{ // get { return UnitVat * Quantity; } // set { vat = value; } //} public virtual decimal GetVat() { return GetUnitVat() * Quantity; }//private decimal subTotal;
//public virtual decimal SubTotal //{ // get { return UnitPrice * Quantity; } // set { subTotal = value; } //} public virtual decimal GetSubTotal() { return UnitPrice * Quantity; }//private decimal total;
//public virtual decimal Total //{ // get { return SubTotal + Vat; } // set { total = value; } //} public virtual decimal GetTotal() { return GetSubTotal() + GetVat(); } }}
Models/OrderRecord.cs:
using Orchard.ContentManagement.Records;
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks;namespace TMinji.Shop.Models
{ public class OrderRecord { public virtual int Id { get; set; } public virtual int CustomerId { get; set; } public virtual DateTime CreatedAt { get; set; } public virtual decimal SubTotal { get; set; } public virtual decimal Vat { get; set; } public virtual OrderStatus Status { get; set; } public virtual IList<OrderDetailRecord> Details { get; private set; } public virtual string PaymentServiceProviderResponse { get; set; } public virtual string PaymentReference { get; set; } public virtual DateTime? PaidAt { get; set; } public virtual DateTime? CompletedAt { get; set; } public virtual DateTime? CancelledAt { get; set; }private decimal total;
//public virtual decimal Total //{ // get { return SubTotal + Vat; } // //private set { total = value; } //}public virtual decimal GetTotal()
{ return SubTotal + Vat; }private string number;
//public virtual string Number //{ // get { return (Id + 1000).ToString(CultureInfo.InvariantCulture); } // //private set { number = value; } //} public virtual string GetNumber() { return (Id + 1000).ToString(CultureInfo.InvariantCulture); }public OrderRecord()
{ Details = new List<OrderDetailRecord>(); }public virtual void UpdateTotals()
{ var subTotal = 0m; var vat = 0m;foreach (var detail in Details)
{ subTotal += detail.GetSubTotal(); vat += detail.GetVat(); }SubTotal = subTotal;
Vat = vat; } }}
Models/OrderStatus.cs:
using System;
using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace TMinji.Shop.Models
{ public enum OrderStatus { /// <summary> /// The order is new and is yet to be paid for /// </summary> New,/// <summary>
/// The order has been paid for, so it's eligable for shipping /// </summary> Paid,/// <summary>
/// The order has shipped /// </summary> Completed,/// <summary>
/// The order was cancelled /// </summary> Cancelled }}
Migrations.cs:
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Common.Fields; using Orchard.Core.Contents.Extensions; using Orchard.Data.Migration; using Orchard.Users.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TMinji.Shop.Models;namespace TMinji.Shop
{ public class Migrations : DataMigrationImpl { public int Create() {SchemaBuilder.CreateTable("ProductPartRecord", table => table
.ContentPartRecord() .Column<decimal>("UnitPrice") .Column<string>("Sku", column => column.WithLength(50)) );return 1;
}public int UpdateFrom1()
{ ContentDefinitionManager.AlterPartDefinition("ProductPart", part => part .Attachable());return 2;
}public int UpdateFrom2()
{ // Define a new content type called "ShoppingCartWidget" ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type // Attach the "ShoppingCartWidgetPart" .WithPart("ShoppingCartWidgetPart") // In order to turn this content type into a widget, it needs the WidgetPart .WithPart("WidgetPart") // It also needs a setting called "Stereotype" to be set to "Widget" .WithSetting("Stereotype", "Widget") );return 3;
}public int UpdateFrom3()
{ // Update the ShoppingCartWidget so that it has a CommonPart attached, which is required for widgets (it's generally a good idea to have this part attached) ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type .WithPart("CommonPart") );return 4;
}public int UpdateFrom4()
{ SchemaBuilder.CreateTable("CustomerPartRecord", table => table .ContentPartRecord() .Column<string>("FirstName", c => c.WithLength(50)) .Column<string>("LastName", c => c.WithLength(50)) .Column<string>("Title", c => c.WithLength(10)) .Column<DateTime>("CreatedUtc") );SchemaBuilder.CreateTable("AddressPartRecord", table => table
.ContentPartRecord() .Column<int>("CustomerId") .Column<string>("Type", c => c.WithLength(50)) );ContentDefinitionManager.AlterPartDefinition("CustomerPart", part => part
.Attachable(false) );ContentDefinitionManager.AlterTypeDefinition("Customer", type => type
.WithPart("CustomerPart") .WithPart("UserPart") );ContentDefinitionManager.AlterPartDefinition("AddressPart", part => part
.Attachable(false) .WithField("Name", f => f.OfType("TextField")) .WithField("AddressLine1", f => f.OfType("TextField")) .WithField("AddressLine2", f => f.OfType("TextField")) .WithField("Zipcode", f => f.OfType("TextField")) .WithField("City", f => f.OfType("TextField")) .WithField("Country", f => f.OfType("TextField")) );ContentDefinitionManager.AlterTypeDefinition("Address", type => type
.WithPart("CommonPart") .WithPart("AddressPart") );return 5;
}public int UpdateFrom5()
{ ContentDefinitionManager.AlterPartDefinition(typeof(CustomerPart).Name, p => p .Attachable(false) .WithField("Phone", f => f.OfType(typeof(TextField).Name)) );ContentDefinitionManager.AlterTypeDefinition("Customer", t => t
.WithPart(typeof(CustomerPart).Name) .WithPart(typeof(UserPart).Name) );ContentDefinitionManager.AlterPartDefinition(typeof(AddressPart).Name, p => p
.Attachable(false) .WithField("Name", f => f.OfType(typeof(TextField).Name)) .WithField("AddressLine1", f => f.OfType(typeof(TextField).Name)) .WithField("AddressLine2", f => f.OfType(typeof(TextField).Name)) .WithField("Zipcode", f => f.OfType(typeof(TextField).Name)) .WithField("City", f => f.OfType(typeof(TextField).Name)) .WithField("Country", f => f.OfType(typeof(TextField).Name)) );ContentDefinitionManager.AlterTypeDefinition("Address", t => t
.WithPart(typeof(AddressPart).Name) );return 6;
}public int UpdateFrom6()
{ //FOREIGN KEY 约束"Order_Customer"冲突。表"dbo.TMinji_Shop_CustomerRecord", column 'Id'。 SchemaBuilder.CreateTable("OrderRecord", t => t .Column<int>("Id", c => c.PrimaryKey().Identity()) .Column<int>("CustomerId", c => c.NotNull()) .Column<DateTime>("CreatedAt", c => c.NotNull()) .Column<decimal>("SubTotal", c => c.NotNull()) .Column<decimal>("Vat", c => c.NotNull()) .Column<string>("Status", c => c.WithLength(50).NotNull()) .Column<string>("PaymentServiceProviderResponse", c => c.WithLength(null)) .Column<string>("PaymentReference", c => c.WithLength(50)) .Column<DateTime>("PaidAt", c => c.Nullable()) .Column<DateTime>("CompletedAt", c => c.Nullable()) .Column<DateTime>("CancelledAt", c => c.Nullable()) );SchemaBuilder.CreateTable("OrderDetailRecord", t => t
.Column<int>("Id", c => c.PrimaryKey().Identity()) .Column<int>("OrderRecord_Id", c => c.NotNull()) .Column<int>("ProductId", c => c.NotNull()) .Column<int>("Quantity", c => c.NotNull()) .Column<decimal>("UnitPrice", c => c.NotNull()) .Column<decimal>("VatRate", c => c.NotNull()) );SchemaBuilder.CreateForeignKey("Order_Customer", "OrderRecord", new[] { "CustomerId" }, "CustomerPartRecord", new[] { "Id" });
SchemaBuilder.CreateForeignKey("OrderDetail_Order", "OrderDetailRecord", new[] { "OrderRecord_Id" }, "OrderRecord", new[] { "Id" }); SchemaBuilder.CreateForeignKey("OrderDetail_Product", "OrderDetailRecord", new[] { "ProductId" }, "ProductPartRecord", new[] { "Id" });return 7;
}}
}
Services/IOrderService.cs:
using Orchard;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TMinji.Shop.Models;namespace TMinji.Shop.Services
{ public interface IOrderService : IDependency { /// <summary> /// Creates a new order based on the specified ShoppingCartItems /// </summary> OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance). /// </summary> IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails); }}
Services/OrderService.cs:
using Orchard;
using Orchard.ContentManagement; using Orchard.Data; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TMinji.Shop.Models;namespace TMinji.Shop.Services
{ public class OrderService : IOrderService { private readonly IDateTimeService _dateTimeService; private readonly IRepository<ProductPartRecord> _productRepository; private readonly IContentManager _contentManager; private readonly IRepository<OrderRecord> _orderRepository; private readonly IRepository<OrderDetailRecord> _orderDetailRepository; private readonly IOrchardServices _orchardServices;public OrderService(
IDateTimeService dateTimeService, IRepository<ProductPartRecord> productRepository, IContentManager contentManager, IRepository<OrderRecord> orderRepository, IRepository<OrderDetailRecord> orderDetailRepository, IOrchardServices orchardServices) { _dateTimeService = dateTimeService; _productRepository = productRepository; _contentManager = contentManager; _orderRepository = orderRepository; _orderDetailRepository = orderDetailRepository; _orchardServices = orchardServices; }public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items)
{if (items == null)
throw new ArgumentNullException("items");// Convert to an array to avoid re-running the enumerable
var itemsArray = items.ToArray();if (!itemsArray.Any())
throw new ArgumentException("Creating an order with 0 items is not supported", "items");var order = new OrderRecord
{ CreatedAt = _dateTimeService.Now, CustomerId = customerId, Status = OrderStatus.New };_orderRepository.Create(order);
// Get all products in one shot, so we can add the product reference to each order detail var productIds = itemsArray.Select(x => x.ProductId).ToArray(); var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();// Create an order detail for each item
foreach (var item in itemsArray) { var product = products.Single(x => x.Id == item.ProductId);var detail = new OrderDetailRecord
{ OrderRecord_Id = order.Id, ProductId = product.Id, Quantity = item.Quantity, UnitPrice = product.UnitPrice, VatRate = .19m };_orderDetailRepository.Create(detail);
order.Details.Add(detail); }order.UpdateTotals();
return order;
}/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance). /// </summary> public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails) { var productIds = orderDetails.Select(x => x.ProductId).ToArray(); return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty); } }}
ResourceManifest.cs:
using Orchard.UI.Resources;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace TMinji.Shop
{ public class ResourceManifest : IResourceManifestProvider { public void BuildManifests(ResourceManifestBuilder builder) { // Create and add a new manifest var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource //manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery"); manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");manifest.DefineStyle("TMinji.Shop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("TMinji.Shop.Common");
manifest.DefineStyle("TMinji.Shop.Order").SetUrl("order.css").SetDependencies("TMinji.Shop.Common");
}
}}
最终结果:
数据库记录为:
二:支付之 Event Bus
Event Bus 这个机制可被用于扩展 Orchard 模块。首先,让我们看看如果 Event Bus 应用到支付中的话,其机制是怎么样的:
首先,我们要定义一个 PaymentRequest,它包含了两个属性:Created Order 和 flag,这能告诉 Event Listener 我们需要开始支付流程,我们还会定义 PaymentResponse,它包含了 payment service provider 的反馈。现在,看代码吧:
TMinji.Shop.Extensibility.PaymentRequest
public class PaymentRequest
{ public OrderRecord Order { get; private set; } public bool WillHandlePayment { get; set; } public ActionResult ActionResult { get; set; }public PaymentRequest(OrderRecord order)
{ Order = order; } }
TMinji.Shop.Extensibility.PaymentResponse
public class PaymentResponse
{ public bool WillHandleResponse { get; set; } public PaymentResponseStatus Status { get; set; } public string OrderReference { get; set; } public string PaymentReference { get; set; } public string ResponseText { get; set; } public HttpContextBase HttpContext { get; private set; }public PaymentResponse(HttpContextBase httpContext)
{ HttpContext = httpContext; } }
TMinji.Shop.Extensibility.PaymentResponseStatus
public enum PaymentResponseStatus
{ Success, Failed, Cancelled, Exception }
Extensibility/IPaymentServiceProvider.cs:
public interface IPaymentServiceProvider : IEventHandler
{ void RequestPayment(PaymentRequest e); void ProcessResponse(PaymentResponse e); }
Controllers/OrderController.cs:
private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders;
private readonly Localizer _t;public OrderController(
IShapeFactory shapeFactory, IOrderService orderService, IAuthenticationService authenticationService, IShoppingCart shoppingCart, ICustomerService customerService, IEnumerable<IPaymentServiceProvider> paymentServiceProviders) { _shapeFactory = shapeFactory; _orderService = orderService; _authenticationService = authenticationService; _shoppingCart = shoppingCart; _customerService = customerService; _paymentServiceProviders = paymentServiceProviders; //_paymentServiceProvider = new SimulatedPaymentServiceProvider(); _t = NullLocalizer.Instance; }
这里,需要特别说明哦:
只要模块中存在 IPaymentServiceProvider 的实现类,注入机制就都会注入进这个列表,这样一来,就实现了 Event Bus
Module.txt:
name: tminji.shop
antiforgery: enabled author: tminji.com website: version: 1.0.0 orchardversion: 1.0.0 description: The tminji.com module is a shopping module. Dependencies: Orchard.Projections, Orchard.Forms, Orchard.jQuery, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout, Orchard.Users features: shop: Description: shopping module. Category: ASample SimulatedPSP: Description: Provides a simulated Payment Service Provider for testing purposes only. Category: ASample
然后,到后台启动我们的支付模块:
Services/SimulatedPaymentServiceProvider.cs:
using Orchard.Environment.Extensions;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc; using System.Web.Routing; using TMinji.Shop.Extensibility;namespace TMinji.Shop.Services
{ [OrchardFeature("TMinji.Shop.SimulatedPSP")] public class SimulatedPaymentServiceProvider : IPaymentServiceProvider { public void RequestPayment(PaymentRequest e) {e.ActionResult = new RedirectToRouteResult(new RouteValueDictionary {
{"action", "Index"}, {"controller", "SimulatedPaymentServiceProvider"}, {"area", "TMinji.Shop"}, {"orderReference", e.Order.GetNumber()}, {"amount", (int)(e.Order.GetTotal() * 100)} });e.WillHandlePayment = true;
}public void ProcessResponse(PaymentResponse e)
{ var result = e.HttpContext.Request.QueryString["result"];e.OrderReference = e.HttpContext.Request.QueryString["orderReference"];
e.PaymentReference = e.HttpContext.Request.QueryString["paymentId"]; e.ResponseText = e.HttpContext.Request.QueryString.ToString();switch (result)
{ case "Success": e.Status = PaymentResponseStatus.Success; break; case "Failure": e.Status = PaymentResponseStatus.Failed; break; case "Cancelled": e.Status = PaymentResponseStatus.Cancelled; break; default: e.Status = PaymentResponseStatus.Exception; break; }e.WillHandleResponse = true;
} }}
Views/SimulatedPaymentServiceProvider/Index.cshtml:
@{
var orderReference = (string)Model.OrderReference; var amount = (decimal)((int)Model.Amount) / 100; var commands = new[] { "Success", "Failure", "Cancelled", "Exception" };Style.Require("TMinji.Shop.SimulatedPSP");
}<h2>Payment Service Provider Simulation</h2>
<p> Received a payment request with order reference <strong>@orderReference</strong><br /> Amount: <strong>@amount.ToString("c")</strong> </p> @using (Html.BeginFormAntiForgeryPost(Url.Action("Command", "SimulatedPaymentServiceProvider", new { area = "TMinji.Shop" }))) { <article class="form"> <input type="hidden" name="orderReference" value="@orderReference" /> <ul class="commands"> @foreach (var command in commands) { <li><button type="submit" name="command" value="@command">@command</button></li> } </ul> </article> }
ResourceManifest.cs:
using Orchard.UI.Resources;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace TMinji.Shop
{ public class ResourceManifest : IResourceManifestProvider { public void BuildManifests(ResourceManifestBuilder builder) { // Create and add a new manifest var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource //manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery"); manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");manifest.DefineStyle("TMinji.Shop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("TMinji.Shop.Common");
manifest.DefineStyle("TMinji.Shop.Order").SetUrl("order.css").SetDependencies("TMinji.Shop.Common");
manifest.DefineStyle("TMinji.Shop.SimulatedPSP").SetUrl("simulated-psp.css").SetDependencies("TMinji.Shop.Common");
}
}}
Controllers/SimulatedPaymentServiceProviderController.cs:
using Orchard.DisplayManagement;
using Orchard.Themes; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc;namespace TMinji.Shop.Controllers
{ public class SimulatedPaymentServiceProviderController : Controller {private readonly dynamic _shapeFactory;
public SimulatedPaymentServiceProviderController(IShapeFactory shapeFactory)
{ _shapeFactory = shapeFactory; }[Themed]
public ActionResult Index(string orderReference, int amount) { var model = _shapeFactory.PaymentRequest( OrderReference: orderReference, Amount: amount );return View(model);
}[HttpPost]
public ActionResult Command(string command, string orderReference) {// Generate a fake payment ID
var paymentId = new Random(Guid.NewGuid().GetHashCode()).Next(1000, 9999);// Redirect back to the webshop
return RedirectToAction("PaymentResponse", "Order", new { area = "TMinji.Shop", paymentId = paymentId, result = command, orderReference }); } } }
Controllers/OrderController.cs:
using Orchard;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc; using Orchard.Mvc; using Orchard.Themes; using Orchard.Localization; using Orchard.Security; using TMinji.Shop.ViewModels; using TMinji.Shop.Services; using TMinji.Shop.Models; using TMinji.Shop.Helpers; using Orchard.ContentManagement; using Orchard.DisplayManagement; using TMinji.Shop.Extensibility;namespace TMinji.Shop.Controllers
{ public class OrderController : Controller { private readonly dynamic _shapeFactory; private readonly IOrderService _orderService; private readonly IAuthenticationService _authenticationService; private readonly IShoppingCart _shoppingCart; private readonly ICustomerService _customerService; private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders; private readonly Localizer _t;public OrderController(
IShapeFactory shapeFactory, IOrderService orderService, IAuthenticationService authenticationService, IShoppingCart shoppingCart, ICustomerService customerService, IEnumerable<IPaymentServiceProvider> paymentServiceProviders) { _shapeFactory = shapeFactory; _orderService = orderService; _authenticationService = authenticationService; _shoppingCart = shoppingCart; _customerService = customerService; _paymentServiceProviders = paymentServiceProviders; //_paymentServiceProvider = new SimulatedPaymentServiceProvider(); _t = NullLocalizer.Instance; }[Themed, HttpPost]
public ActionResult Create() {var user = _authenticationService.GetAuthenticatedUser();
if (user == null)
throw new OrchardSecurityException(_t("Login required"));var customer = user.ContentItem.As<CustomerPart>();
if (customer == null)
throw new InvalidOperationException("The current user is not a customer");var order = _orderService.CreateOrder(customer.Id, _shoppingCart.Items);
// Fire the PaymentRequest event
var paymentRequest = new PaymentRequest(order);foreach (var handler in _paymentServiceProviders)
{ handler.RequestPayment(paymentRequest);// If the handler responded, it will set the action result
if (paymentRequest.WillHandlePayment) { return paymentRequest.ActionResult; } }// If we got here, no PSP handled the OrderCreated event, so we'll just display the order.
var shape = _shapeFactory.Order_Created( Order: order, Products: _orderService.GetProducts(order.Details).ToArray(), Customer: customer, InvoiceAddress: (dynamic)_customerService.GetAddress(user.Id, "InvoiceAddress"), ShippingAddress: (dynamic)_customerService.GetAddress(user.Id, "ShippingAddress") ); return new ShapeResult(this, shape); }[Themed]
public ActionResult PaymentResponse() {var args = new PaymentResponse(HttpContext);
foreach (var handler in _paymentServiceProviders)
{ handler.ProcessResponse(args);if (args.WillHandleResponse)
break; }if (!args.WillHandleResponse)
throw new OrchardException(_t("Such things mean trouble"));var order = _orderService.GetOrderByNumber(args.OrderReference);
_orderService.UpdateOrderStatus(order, args);if (order.Status == OrderStatus.Paid)
{ // Send some notification mail message to the customer that the order was paid. // We may also initiate the shipping process from here }return new ShapeResult(this, _shapeFactory.Order_PaymentResponse(Order: order, PaymentResponse: args));
} }}
Services/IOrderService.cs:
using Orchard;
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TMinji.Shop.Extensibility; using TMinji.Shop.Models;namespace TMinji.Shop.Services
{ public interface IOrderService : IDependency { /// <summary> /// Creates a new order based on the specified ShoppingCartItems /// </summary> OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance). /// </summary> IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails);OrderRecord GetOrderByNumber(string orderNumber);
void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse);
}}
Services/OrderService.cs:
using Orchard;
using Orchard.ContentManagement; using Orchard.Data; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using TMinji.Shop.Extensibility; using TMinji.Shop.Models;namespace TMinji.Shop.Services
{ public class OrderService : IOrderService { private readonly IDateTimeService _dateTimeService; private readonly IRepository<ProductPartRecord> _productRepository; private readonly IContentManager _contentManager; private readonly IRepository<OrderRecord> _orderRepository; private readonly IRepository<OrderDetailRecord> _orderDetailRepository; private readonly IOrchardServices _orchardServices;public OrderService(
IDateTimeService dateTimeService, IRepository<ProductPartRecord> productRepository, IContentManager contentManager, IRepository<OrderRecord> orderRepository, IRepository<OrderDetailRecord> orderDetailRepository, IOrchardServices orchardServices) { _dateTimeService = dateTimeService; _productRepository = productRepository; _contentManager = contentManager; _orderRepository = orderRepository; _orderDetailRepository = orderDetailRepository; _orchardServices = orchardServices; }public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items)
{if (items == null)
throw new ArgumentNullException("items");// Convert to an array to avoid re-running the enumerable
var itemsArray = items.ToArray();if (!itemsArray.Any())
throw new ArgumentException("Creating an order with 0 items is not supported", "items");var order = new OrderRecord
{ CreatedAt = _dateTimeService.Now, CustomerId = customerId, Status = OrderStatus.New };_orderRepository.Create(order);
// Get all products in one shot, so we can add the product reference to each order detail var productIds = itemsArray.Select(x => x.ProductId).ToArray(); var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();// Create an order detail for each item
foreach (var item in itemsArray) { var product = products.Single(x => x.Id == item.ProductId);var detail = new OrderDetailRecord
{ OrderRecord_Id = order.Id, ProductId = product.Id, Quantity = item.Quantity, UnitPrice = product.UnitPrice, VatRate = .19m };_orderDetailRepository.Create(detail);
order.Details.Add(detail); }order.UpdateTotals();
return order;
}/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance). /// </summary> public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails) { var productIds = orderDetails.Select(x => x.ProductId).ToArray(); return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty); }public OrderRecord GetOrderByNumber(string orderNumber)
{ var orderId = int.Parse(orderNumber) - 1000; return _orderRepository.Get(orderId); }public void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse)
{ OrderStatus orderStatus;switch (paymentResponse.Status)
{ case PaymentResponseStatus.Success: orderStatus = OrderStatus.Paid; break; default: orderStatus = OrderStatus.Cancelled; break; }if (order.Status == orderStatus)
return;order.Status = orderStatus;
order.PaymentServiceProviderResponse = paymentResponse.ResponseText; order.PaymentReference = paymentResponse.PaymentReference;switch (order.Status)
{ case OrderStatus.Paid: order.PaidAt = _dateTimeService.Now; break; case OrderStatus.Completed: order.CompletedAt = _dateTimeService.Now; break; case OrderStatus.Cancelled: order.CancelledAt = _dateTimeService.Now; break; } }}
}
Views/Order.PaymentResponse.cshtml:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models @using TMinji.Shop.Models @using TMinji.Shop.Extensibility @using Orchard.Core; @{ var order = (OrderRecord)Model.Order; var paymentResponse = (PaymentResponse)Model.PaymentResponse; } @if (paymentResponse.Status == PaymentResponseStatus.Success) { <h2>@T("Payment was succesful")</h2> <p>Thanks! We succesfully received payment for order @order.GetNumber() with payment ID @paymentResponse.PaymentReference</p> <p>Enjoy your products and come again!</p> } else { <h2>@T("Order cancelled")</h2> <p>Your order (@order.GetNumber()) has been cancelled</p> }
最终结果如下:
数据库结果:
本文转自最课程陆敏技博客园博客,原文链接:http://www.cnblogs.com/luminji/p/3862704.html,如需转载请自行联系原作者