在这部分,我们要完成的工作有:
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: http://www.tminji.com
version: 1.0.0
orchardversion: 1.0.0
description: The tminji.com module is a shopping module.
Dependencies: Orchard.Projections, Orchard.Forms, Orchard.jQuery, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout, Orchard.Users
features:
shop:
Description: shopping module.
Category: ASample
SimulatedPSP:
Description: Provides a simulated Payment Service Provider for testing purposes only.
Category: ASample
然后,到后台启动我们的支付模块:
Services/SimulatedPaymentServiceProvider.cs:
using Orchard.Environment.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using TMinji.Shop.Extensibility;namespace TMinji.Shop.Services
{
[OrchardFeature("TMinji.Shop.SimulatedPSP")]
public class SimulatedPaymentServiceProvider : IPaymentServiceProvider
{
public void RequestPayment(PaymentRequest e)
{e.ActionResult = new RedirectToRouteResult(new RouteValueDictionary {
{"action", "Index"},
{"controller", "SimulatedPaymentServiceProvider"},
{"area", "TMinji.Shop"},
{"orderReference", e.Order.GetNumber()},
{"amount", (int)(e.Order.GetTotal() * 100)}
});e.WillHandlePayment = true;
}public void ProcessResponse(PaymentResponse e)
{
var result = e.HttpContext.Request.QueryString["result"];e.OrderReference = e.HttpContext.Request.QueryString["orderReference"];
e.PaymentReference = e.HttpContext.Request.QueryString["paymentId"];
e.ResponseText = e.HttpContext.Request.QueryString.ToString();switch (result)
{
case "Success":
e.Status = PaymentResponseStatus.Success;
break;
case "Failure":
e.Status = PaymentResponseStatus.Failed;
break;
case "Cancelled":
e.Status = PaymentResponseStatus.Cancelled;
break;
default:
e.Status = PaymentResponseStatus.Exception;
break;
}e.WillHandleResponse = true;
}
}}
Views/SimulatedPaymentServiceProvider/Index.cshtml:
@{
var orderReference = (string)Model.OrderReference;
var amount = (decimal)((int)Model.Amount) / 100;
var commands = new[] { "Success", "Failure", "Cancelled", "Exception" };Style.Require("TMinji.Shop.SimulatedPSP");
}<h2>Payment Service Provider Simulation</h2>
<p>
Received a payment request with order reference <strong>@orderReference</strong><br />
Amount: <strong>@amount.ToString("c")</strong>
</p>
@using (Html.BeginFormAntiForgeryPost(Url.Action("Command", "SimulatedPaymentServiceProvider", new { area = "TMinji.Shop" })))
{
<article class="form">
<input type="hidden" name="orderReference" value="@orderReference" />
<ul class="commands">
@foreach (var command in commands)
{
<li><button type="submit" name="command" value="@command">@command</button></li>
}
</ul>
</article>
}
ResourceManifest.cs:
using Orchard.UI.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace TMinji.Shop
{
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifests(ResourceManifestBuilder builder)
{
// Create and add a new manifest
var manifest = builder.Add();// Define a "common" style sheet
manifest.DefineStyle("TMinji.Shop.Common").SetUrl("common.css");// Define the "shoppingcart" style sheet
manifest.DefineStyle("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("TMinji.Shop.Common");manifest.DefineStyle("TMinji.Shop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
//manifest.DefineScript("jQuery").SetUrl("jquery-1.9.1.min.js", "jquery-1.9.1.js").SetVersion("1.9.1");
// Define the "shoppingcart" script and set a dependency on the "jQuery" resource
//manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
manifest.DefineScript("TMinji.Shop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery", "jQuery_LinqJs", "ko");manifest.DefineStyle("TMinji.Shop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("TMinji.Shop.Common");
manifest.DefineStyle("TMinji.Shop.Order").SetUrl("order.css").SetDependencies("TMinji.Shop.Common");
manifest.DefineStyle("TMinji.Shop.SimulatedPSP").SetUrl("simulated-psp.css").SetDependencies("TMinji.Shop.Common");
}
}}
Controllers/SimulatedPaymentServiceProviderController.cs:
using Orchard.DisplayManagement;
using Orchard.Themes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;namespace TMinji.Shop.Controllers
{
public class SimulatedPaymentServiceProviderController : Controller
{private readonly dynamic _shapeFactory;
public SimulatedPaymentServiceProviderController(IShapeFactory shapeFactory)
{
_shapeFactory = shapeFactory;
}[Themed]
public ActionResult Index(string orderReference, int amount)
{
var model = _shapeFactory.PaymentRequest(
OrderReference: orderReference,
Amount: amount
);return View(model);
}[HttpPost]
public ActionResult Command(string command, string orderReference)
{// Generate a fake payment ID
var paymentId = new Random(Guid.NewGuid().GetHashCode()).Next(1000, 9999);// Redirect back to the webshop
return RedirectToAction("PaymentResponse", "Order", new { area = "TMinji.Shop", paymentId = paymentId, result = command, orderReference });
}
}
}
Controllers/OrderController.cs:
using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using Orchard.Mvc;
using Orchard.Themes;
using Orchard.Localization;
using Orchard.Security;
using TMinji.Shop.ViewModels;
using TMinji.Shop.Services;
using TMinji.Shop.Models;
using TMinji.Shop.Helpers;
using Orchard.ContentManagement;
using Orchard.DisplayManagement;
using TMinji.Shop.Extensibility;namespace TMinji.Shop.Controllers
{
public class OrderController : Controller
{
private readonly dynamic _shapeFactory;
private readonly IOrderService _orderService;
private readonly IAuthenticationService _authenticationService;
private readonly IShoppingCart _shoppingCart;
private readonly ICustomerService _customerService;
private readonly IEnumerable<IPaymentServiceProvider> _paymentServiceProviders;
private readonly Localizer _t;public OrderController(
IShapeFactory shapeFactory,
IOrderService orderService,
IAuthenticationService authenticationService,
IShoppingCart shoppingCart,
ICustomerService customerService,
IEnumerable<IPaymentServiceProvider> paymentServiceProviders)
{
_shapeFactory = shapeFactory;
_orderService = orderService;
_authenticationService = authenticationService;
_shoppingCart = shoppingCart;
_customerService = customerService;
_paymentServiceProviders = paymentServiceProviders;
//_paymentServiceProvider = new SimulatedPaymentServiceProvider();
_t = NullLocalizer.Instance;
}[Themed, HttpPost]
public ActionResult Create()
{var user = _authenticationService.GetAuthenticatedUser();
if (user == null)
throw new OrchardSecurityException(_t("Login required"));var customer = user.ContentItem.As<CustomerPart>();
if (customer == null)
throw new InvalidOperationException("The current user is not a customer");var order = _orderService.CreateOrder(customer.Id, _shoppingCart.Items);
// Fire the PaymentRequest event
var paymentRequest = new PaymentRequest(order);foreach (var handler in _paymentServiceProviders)
{
handler.RequestPayment(paymentRequest);// If the handler responded, it will set the action result
if (paymentRequest.WillHandlePayment)
{
return paymentRequest.ActionResult;
}
}// If we got here, no PSP handled the OrderCreated event, so we'll just display the order.
var shape = _shapeFactory.Order_Created(
Order: order,
Products: _orderService.GetProducts(order.Details).ToArray(),
Customer: customer,
InvoiceAddress: (dynamic)_customerService.GetAddress(user.Id, "InvoiceAddress"),
ShippingAddress: (dynamic)_customerService.GetAddress(user.Id, "ShippingAddress")
);
return new ShapeResult(this, shape);
}[Themed]
public ActionResult PaymentResponse()
{var args = new PaymentResponse(HttpContext);
foreach (var handler in _paymentServiceProviders)
{
handler.ProcessResponse(args);if (args.WillHandleResponse)
break;
}if (!args.WillHandleResponse)
throw new OrchardException(_t("Such things mean trouble"));var order = _orderService.GetOrderByNumber(args.OrderReference);
_orderService.UpdateOrderStatus(order, args);if (order.Status == OrderStatus.Paid)
{
// Send some notification mail message to the customer that the order was paid.
// We may also initiate the shipping process from here
}return new ShapeResult(this, _shapeFactory.Order_PaymentResponse(Order: order, PaymentResponse: args));
}
}}
Services/IOrderService.cs:
using Orchard;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Extensibility;
using TMinji.Shop.Models;namespace TMinji.Shop.Services
{
public interface IOrderService : IDependency
{
/// <summary>
/// Creates a new order based on the specified ShoppingCartItems
/// </summary>
OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items);/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
/// </summary>
IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails);OrderRecord GetOrderByNumber(string orderNumber);
void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse);
}}
Services/OrderService.cs:
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TMinji.Shop.Extensibility;
using TMinji.Shop.Models;namespace TMinji.Shop.Services
{
public class OrderService : IOrderService
{
private readonly IDateTimeService _dateTimeService;
private readonly IRepository<ProductPartRecord> _productRepository;
private readonly IContentManager _contentManager;
private readonly IRepository<OrderRecord> _orderRepository;
private readonly IRepository<OrderDetailRecord> _orderDetailRepository;
private readonly IOrchardServices _orchardServices;public OrderService(
IDateTimeService dateTimeService,
IRepository<ProductPartRecord> productRepository,
IContentManager contentManager,
IRepository<OrderRecord> orderRepository,
IRepository<OrderDetailRecord> orderDetailRepository,
IOrchardServices orchardServices)
{
_dateTimeService = dateTimeService;
_productRepository = productRepository;
_contentManager = contentManager;
_orderRepository = orderRepository;
_orderDetailRepository = orderDetailRepository;
_orchardServices = orchardServices;
}public OrderRecord CreateOrder(int customerId, IEnumerable<ShoppingCartItem> items)
{if (items == null)
throw new ArgumentNullException("items");// Convert to an array to avoid re-running the enumerable
var itemsArray = items.ToArray();if (!itemsArray.Any())
throw new ArgumentException("Creating an order with 0 items is not supported", "items");var order = new OrderRecord
{
CreatedAt = _dateTimeService.Now,
CustomerId = customerId,
Status = OrderStatus.New
};_orderRepository.Create(order);
// Get all products in one shot, so we can add the product reference to each order detail
var productIds = itemsArray.Select(x => x.ProductId).ToArray();
var products = _productRepository.Fetch(x => productIds.Contains(x.Id)).ToArray();// Create an order detail for each item
foreach (var item in itemsArray)
{
var product = products.Single(x => x.Id == item.ProductId);var detail = new OrderDetailRecord
{
OrderRecord_Id = order.Id,
ProductId = product.Id,
Quantity = item.Quantity,
UnitPrice = product.UnitPrice,
VatRate = .19m
};_orderDetailRepository.Create(detail);
order.Details.Add(detail);
}order.UpdateTotals();
return order;
}/// <summary>
/// Gets a list of ProductParts from the specified list of OrderDetails. Useful if you need to use the product as a ProductPart (instead of just having access to the ProductRecord instance).
/// </summary>
public IEnumerable<ProductPart> GetProducts(IEnumerable<OrderDetailRecord> orderDetails)
{
var productIds = orderDetails.Select(x => x.ProductId).ToArray();
return _contentManager.GetMany<ProductPart>(productIds, VersionOptions.Latest, QueryHints.Empty);
}public OrderRecord GetOrderByNumber(string orderNumber)
{
var orderId = int.Parse(orderNumber) - 1000;
return _orderRepository.Get(orderId);
}public void UpdateOrderStatus(OrderRecord order, PaymentResponse paymentResponse)
{
OrderStatus orderStatus;switch (paymentResponse.Status)
{
case PaymentResponseStatus.Success:
orderStatus = OrderStatus.Paid;
break;
default:
orderStatus = OrderStatus.Cancelled;
break;
}if (order.Status == orderStatus)
return;order.Status = orderStatus;
order.PaymentServiceProviderResponse = paymentResponse.ResponseText;
order.PaymentReference = paymentResponse.PaymentReference;switch (order.Status)
{
case OrderStatus.Paid:
order.PaidAt = _dateTimeService.Now;
break;
case OrderStatus.Completed:
order.CompletedAt = _dateTimeService.Now;
break;
case OrderStatus.Cancelled:
order.CancelledAt = _dateTimeService.Now;
break;
}
}}
}
Views/Order.PaymentResponse.cshtml:
@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using TMinji.Shop.Models
@using TMinji.Shop.Extensibility
@using Orchard.Core;
@{
var order = (OrderRecord)Model.Order;
var paymentResponse = (PaymentResponse)Model.PaymentResponse;
}
@if (paymentResponse.Status == PaymentResponseStatus.Success)
{
<h2>@T("Payment was succesful")</h2>
<p>Thanks! We succesfully received payment for order @order.GetNumber() with payment ID @paymentResponse.PaymentReference</p>
<p>Enjoy your products and come again!</p>
}
else
{
<h2>@T("Order cancelled")</h2>
<p>Your order (@order.GetNumber()) has been cancelled</p>
}
最终结果如下:
数据库结果: