重要通知:BrnShop企業版NOSQL設計(基於Redis)已經開源!源碼內置於最新版的BrnShop中,感興趣的園友可以去下載來看看。官網地址:www.brnshop.com。
好了現在進入今天的正題:自定義插件。上一講中我們已經闡述了BrnShop插件的工作機制,現在我們詳細介紹下如何自定義插件。首先BrnShop的插件從功能上分為三類,分別是:
- 開放授權插件(OAuth)
- 支付插件
- 配送插件
對應的接口文件(注:位於BrnShop.Core項目的Plugin/Base文件夾中)依次如下:
- IOAuthPlugin
- IPayPlugin
- IShipPlugin
現在我們依次介紹下各個接口,首先登場的是IOAuthPlugin接口。先看它的定義:
/// <summary>
/// BrnShop開放授權插件接口
/// </summary>
public interface IOAuthPlugin : IPlugin
{
/// <summary>
/// 登陸控制器
/// </summary>
string LoginController { get; }
/// <summary>
/// 登陸動作方法
/// </summary>
string LoginAction { get; }
/// <summary>
/// 登陸路由數據
/// </summary>
RouteValueDictionary LoginRouteValues { get; }
}
對於一個開放授權插件來說,它只需要向主應用程序提供自己的一個登陸地址就可以,至於怎么授權驗證等那都是插件自己的事情了。所以IOAuthPlugin接口內容僅僅是登陸地址的mvc3要素(控制器名,動作方法名,路由數據)就可以了,以QQ授權登陸為例:

接下來是IPayPlugin接口,代碼如下:
/// <summary>
/// BrnShop支付插件接口
/// </summary>
public interface IPayPlugin : IPlugin
{
/// <summary>
/// 付款方式(0代表貨到付款,1代表在線付款,2代表線下付款)
/// </summary>
int PayMode { get; }
/// <summary>
/// 是否允許賬戶充值(只對在線付款有效)
/// </summary>
bool AllowRecharge { get; }
/// <summary>
/// 支付返回控制器
/// </summary>
string ReturnController { get; }
/// <summary>
/// 支付返回動作方法
/// </summary>
string ReturnAction { get; }
/// <summary>
/// 支付返回路由數據
/// </summary>
RouteValueDictionary ReturnRouteValues { get; }
/// <summary>
/// 支付通知控制器
/// </summary>
string NotifyController { get; }
/// <summary>
/// 支付通知動作方法
/// </summary>
string NotifyAction { get; }
/// <summary>
/// 支付通知路由數據
/// </summary>
RouteValueDictionary NotifyRouteValues { get; }
/// <summary>
/// 獲得支付手續費
/// </summary>
/// <param name="productAmount">商品合計</param>
/// <param name="buyTime">購買時間</param>
/// <param name="partUserInfo">購買用戶</param>
/// <returns></returns>
decimal GetPayFee(decimal productAmount, DateTime buyTime, PartUserInfo partUserInfo);
/// <summary>
/// 如果付款方式為在線付款則返回付款請求的url,否則返回空字符串
/// </summary>
/// <param name="returnUrl">返回url</param>
/// <param name="notifyUrl">通知url</param>
/// <param name="pluginInfo">插件信息</param>
/// <param name="partUserInfo">購買用戶</param>
/// <param name="orderInfo">訂單信息</param>
/// <returns></returns>
string GetRequestUrl(string returnUrl, string notifyUrl, PluginInfo pluginInfo, PartUserInfo partUserInfo, OrderInfo orderInfo);
}
成員比較多,我們分類來看就清晰了:
- PayMode:代表支付的3種類型,這個屬性非常重要,因為其它成員的實現跟他密切相關。
- GetPayFee:計算訂單的支付手續費。
- GetRequestUrl:只在PayMode為在線付款時有效;返回支付地址,在BrnShop.Web項目的OrderController類的PayShow方法中調用。
- ReturnController,ReturnAction,ReturnRouteValues:這3個成員為一組,並且只在PayMode為在線付款時有效;提供支付完成后的返回地址。
- NotifyController,NotifyAction,NotifyRouteValues:這3個成員為一組,並且只在PayMode為在線付款時有效;提供支付完成后的回調地址。
- AllowRecharge:系統預留成員,目前無用。
最后我們來看下IShipPlugin接口,代碼如下:
/// <summary>
/// BrnShop配送插件接口
/// </summary>
public interface IShipPlugin : IPlugin
{
/// <summary>
/// 獲得配送費用
/// </summary>
/// <param name="totalWeight">訂單總重量</param>
/// <param name="productAmount">商品合計</param>
/// <param name="buyTime">購買時間</param>
/// <param name="shipRegionId">收貨區域</param>
/// <param name="partUserInfo">購買用戶</param>
/// <returns></returns>
decimal GetShipFee(int totalWeight, decimal productAmount, DateTime buyTime, int shipRegionId, PartUserInfo partUserInfo);
}
這個接口比較簡單,只是提供一個計算配送費用的成員。
補充說明一下:以上3個接口都繼承自IPlugin,這個接口提供后台配置地址mvc3要素,只在BrnShop.Web.Admin項目中的插件配置視圖中使用,具體如下:
@if (Model.ConfigRouteValues == null)
{
@Html.Action(Model.ConfigAction, Model.ConfigController)
}
else
{
@Html.Action(Model.ConfigAction, Model.ConfigController, Model.ConfigRouteValues)
}
下面我們以支付寶插件為例來講解下如何自定義一個插件。
首先新建一個ASP.NET MVC3應用程序並引用需要的程序集,根據上一篇講解我們知道需要修改非.net自帶程序集的復制到本地屬性和項目生成屬性。具體如下圖:


至此我們的項目基本框架已經搭好了。現在我們需要添加一個插件說明文件(注:此文件為必須文件且文件名稱必須為PluginInfo.config,還有不要忘記修改它的復制屬性)。內容如下:
<?xml version="1.0" encoding="utf-8"?> <PluginInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <SystemName>alipay</SystemName> <FriendlyName>支付寶</FriendlyName> <ClassFullName>BrnShop.PayPlugin.Alipay.PluginService,BrnShop.PayPlugin.Alipay</ClassFullName> <Folder>BrnShop.PayPlugin.Alipay</Folder> <Description>阿里巴巴旗下支付工具</Description> <Type>1</Type> <Author>brnshop</Author> <Version>1.0</Version> <SupVersion>1.0.0</SupVersion> <DisplayOrder>3</DisplayOrder> <IsDefault>0</IsDefault> </PluginInfo>
這個文件是BrnShop.Core項目中的PluginInfo序列化文件,所以節點說明請參考下面代碼:
/// <summary>
/// 插件信息類
/// </summary>
public class PluginInfo : IComparable
{
private string _systemname = "";//插件系統名稱(必須具有唯一性)
private string _friendlyname = "";//插件友好名稱
private string _classfullname = "";//插件控制器
private string _folder = "";//插件目錄
private string _description = "";//插件描述
private int _type = 0;//插件類型(0代表開放授權插件,1代表支付插件,2代表配送插件)
private string _author = "";//插件作者
private string _version = "";//插件版本
private string _supversion = "";//插件支持的BrnShop版本
private int _displayOrder = 0;//插件順序
private int _isdefault = 0;//是否是默認插件
/// <summary>
/// 插件系統名稱
/// </summary>
public string SystemName
{
get { return _systemname; }
set { _systemname = value; }
}
/// <summary>
/// 插件友好名稱
/// </summary>
public string FriendlyName
{
get { return _friendlyname; }
set { _friendlyname = value; }
}
/// <summary>
/// 插件類型名稱
/// </summary>
public string ClassFullName
{
get { return _classfullname; }
set { _classfullname = value; }
}
/// <summary>
/// 插件目錄
/// </summary>
public string Folder
{
get { return _folder; }
set { _folder = value; }
}
/// <summary>
/// 插件描述
/// </summary>
public string Description
{
get { return _description; }
set { _description = value; }
}
/// <summary>
/// 插件類型(0代表開放授權插件,1代表支付插件,2代表配送插件)
/// </summary>
public int Type
{
get { return _type; }
set { _type = value; }
}
/// <summary>
/// 插件作者
/// </summary>
public string Author
{
get { return _author; }
set { _author = value; }
}
/// <summary>
/// 插件版本
/// </summary>
public string Version
{
get { return _version; }
set { _version = value; }
}
/// <summary>
/// 插件支持的BrnShop版本
/// </summary>
public string SupVersion
{
get { return _supversion; }
set { _supversion = value; }
}
/// <summary>
/// 插件順序
/// </summary>
public int DisplayOrder
{
get { return _displayOrder; }
set { _displayOrder = value; }
}
/// <summary>
/// 是否是默認插件
/// </summary>
public int IsDefault
{
get { return _isdefault; }
set { _isdefault = value; }
}
/// <summary>
/// 插件實例
/// </summary>
private IPlugin _instance = null;
/// <summary>
/// 插件實例
/// </summary>
[XmlIgnoreAttribute]
public IPlugin Instance
{
get
{
if (_instance == null)
{
try
{
_instance = (IPlugin)Activator.CreateInstance(System.Type.GetType(ClassFullName, false, true));
}
catch (Exception ex)
{
throw new BSPException("創建插件:" + _classfullname + "的實例失敗", ex);
}
}
return _instance;
}
}
public int CompareTo(object obj)
{
PluginInfo info = (PluginInfo)obj;
if (this.DisplayOrder > info.DisplayOrder)
return 1;
else if (this.DisplayOrder < info.DisplayOrder)
return -1;
else
return 0;
}
}
接下來我們定義一個類PluginService,並實現接口IPayPlugin,代碼如下:
/// <summary>
/// 插件服務類
/// </summary>
public class PluginService : IPayPlugin
{
/// <summary>
/// 插件配置控制器
/// </summary>
/// <value></value>
public string ConfigController
{
get { return "AdminAlipay"; }
}
/// <summary>
/// 插件配置動作方法
/// </summary>
/// <value></value>
public string ConfigAction
{
get { return "Config"; }
}
/// <summary>
/// 插件配置路由數據
/// </summary>
/// <value></value>
public RouteValueDictionary ConfigRouteValues
{
get { return new RouteValueDictionary() { { "area", "Admin" } }; }
}
/// <summary>
/// 付款方式(0代表貨到付款,1代表在線付款,2代表線下付款)
/// </summary>
/// <value></value>
public int PayMode
{
get { return 1; }
}
/// <summary>
/// 是否允許賬戶充值(只對在線付款有效)
/// </summary>
/// <value></value>
public bool AllowRecharge
{
get { return PluginUtils.GetPluginSet().AllowRecharge == 1; }
}
/// <summary>
/// 支付返回控制器
/// </summary>
/// <value></value>
public string ReturnController
{
get { return "Alipay"; }
}
/// <summary>
/// 支付返回動作方法
/// </summary>
/// <value></value>
public string ReturnAction
{
get { return "Return"; }
}
/// <summary>
/// 支付返回路由數據
/// </summary>
/// <value></value>
public RouteValueDictionary ReturnRouteValues
{
get { return null; }
}
/// <summary>
/// 支付通知控制器
/// </summary>
/// <value></value>
public string NotifyController
{
get { return "Alipay"; }
}
/// <summary>
/// 支付通知動作方法
/// </summary>
/// <value></value>
public string NotifyAction
{
get { return "Notify"; }
}
/// <summary>
/// 支付通知路由數據
/// </summary>
/// <value></value>
public RouteValueDictionary NotifyRouteValues
{
get { return null; }
}
/// <summary>
/// 獲得支付手續費
/// </summary>
/// <param name="productAmount">商品合計</param>
/// <param name="buyTime">購買時間</param>
/// <param name="partUserInfo">購買用戶</param>
/// <returns></returns>
public decimal GetPayFee(decimal productAmount, DateTime buyTime, PartUserInfo partUserInfo)
{
return 0M;
}
/// <summary>
/// 如果付款方式為在線付款則返回付款請求的url,否則返回空字符串
/// </summary>
/// <param name="notifyUrl">通知url</param>
/// <param name="returnUrl">返回url</param>
/// <param name="pluginInfo">插件信息</param>
/// <param name="partUserInfo">購買用戶</param>
/// <param name="orderInfo">訂單信息</param>
/// <returns></returns>
public string GetRequestUrl(string notifyUrl, string returnUrl, PluginInfo pluginInfo, PartUserInfo partUserInfo, OrderInfo orderInfo)
{
//支付類型,必填,不能修改
string paymentType = "1";
//服務器異步通知頁面路徑,需http://格式的完整路徑,不能加?id=123這類自定義參數
notifyUrl = string.Format("http://{0}{1}", BSPConfig.ShopConfig.SiteUrl, notifyUrl);
//頁面跳轉同步通知頁面路徑,需http://格式的完整路徑,不能加?id=123這類自定義參數,不能寫成http://localhost/
returnUrl = string.Format("http://{0}{1}", BSPConfig.ShopConfig.SiteUrl, returnUrl);
//收款支付寶帳戶
string sellerEmail = AlipayConfig.Seller;
//合作者身份ID
string partner = AlipayConfig.Partner;
//交易安全檢驗碼
string key = AlipayConfig.Key;
//商戶訂單號
string outTradeNo = orderInfo.Oid.ToString();
//訂單名稱
string subject = "";
//付款金額
string totalFee = orderInfo.SurplusMoney.ToString();
//訂單描述
string body = "";
//防釣魚時間戳,若要使用請調用類文件submit中的query_timestamp函數
string anti_phishing_key = "";
//客戶端的IP地址,非局域網的外網IP地址,如:221.0.0.1
string exter_invoke_ip = "";
//把請求參數打包成數組
SortedDictionary<string, string> sParaTemp = new SortedDictionary<string, string>();
sParaTemp.Add("partner", partner);
sParaTemp.Add("_input_charset", key);
sParaTemp.Add("service", "create_direct_pay_by_user");
sParaTemp.Add("payment_type", paymentType);
sParaTemp.Add("notify_url", notifyUrl);
sParaTemp.Add("return_url", returnUrl);
sParaTemp.Add("seller_email", sellerEmail);
sParaTemp.Add("out_trade_no", outTradeNo);
sParaTemp.Add("subject", subject);
sParaTemp.Add("total_fee", totalFee);
sParaTemp.Add("body", body);
sParaTemp.Add("anti_phishing_key", anti_phishing_key);
sParaTemp.Add("exter_invoke_ip", exter_invoke_ip);
return AlipaySubmit.BuildRequestUrl(sParaTemp, AlipayConfig.Gateway, AlipayConfig.InputCharset, AlipayConfig.SignType, AlipayConfig.Key, AlipayConfig.Code);
}
}
因為支付寶有一些需要配置的屬性要保存,例如收款支付寶帳戶等。在此我們采用對象序列化和文件的方式來保存這些信息。首先定義一個設置信息類,代碼如下:
/// <summary>
/// 插件設置信息類
/// </summary>
public class PluginSetInfo
{
private string _partner;//合作者身份ID
private string _key;//交易安全檢驗碼
private string _seller;//收款支付寶帳戶
private int _allowrecharge;//是否允許賬戶充值
/// <summary>
/// 合作者身份ID
/// </summary>
public string Partner
{
get { return _partner; }
set { _partner = value; }
}
/// <summary>
/// 交易安全檢驗碼
/// </summary>
public string Key
{
get { return _key; }
set { _key = value; }
}
/// <summary>
/// 收款支付寶帳戶
/// </summary>
public string Seller
{
get { return _seller; }
set { _seller = value; }
}
/// <summary>
/// 是否允許賬戶充值
/// </summary>
public int AllowRecharge
{
get { return _allowrecharge; }
set { _allowrecharge = value; }
}
}
然后添加一個工具類PluginUtils來序列化和反序列化設置信息,代碼如下:
/// <summary>
/// 插件工具類
/// </summary>
public class PluginUtils
{
private static object _locker = new object();//鎖對象
private static PluginSetInfo _pluginsetinfo = null;//插件設置信息
private static string _plugindatafilepath = "/Plugins/BrnShop.PayPlugin.Alipay/PluginData.config";//數據文件路徑
/// <summary>
///獲得插件設置
/// </summary>
/// <returns></returns>
public static PluginSetInfo GetPluginSet()
{
if (_pluginsetinfo == null)
{
lock (_locker)
{
if (_pluginsetinfo == null)
{
_pluginsetinfo = (PluginSetInfo)IOHelper.DeserializeFromXML(typeof(PluginSetInfo), IOHelper.GetMapPath(_plugindatafilepath));
}
}
}
return _pluginsetinfo;
}
/// <summary>
/// 保存插件設置到數據數據文件中
/// </summary>
public static void SavePluginSet(PluginSetInfo pluginSetInfo)
{
lock (_locker)
{
IOHelper.SerializeToXml(pluginSetInfo, IOHelper.GetMapPath(_plugindatafilepath));
_pluginsetinfo = null;
AlipayConfig.ReSet();
}
}
}
就下來就是前台和后台實現了。首先是后台實現,對於后台我們只需要提供一個可供修改支付寶配置的子方法就行了,此子方法在上面講解"IPlugin"接口時已經給出了它的調用方式。代碼如下:
/// <summary>
/// 后台支付寶插件控制器類
/// </summary>
public class AdminAlipayController : BaseAdminController
{
/// <summary>
/// 配置
/// </summary>
[HttpGet]
[ChildActionOnly]
public ActionResult Config()
{
ConfigModel model = new ConfigModel();
model.Partner = PluginUtils.GetPluginSet().Partner;
model.Key = PluginUtils.GetPluginSet().Key;
model.Seller = PluginUtils.GetPluginSet().Seller;
model.AllowRecharge = PluginUtils.GetPluginSet().AllowRecharge;
return View("~/Plugins/BrnShop.PayPlugin.Alipay/Views/AdminAlipay/Config.cshtml", model);
}
/// <summary>
/// 配置
/// </summary>
[HttpPost]
public ActionResult Config(ConfigModel model)
{
if (ModelState.IsValid)
{
PluginSetInfo setting = new PluginSetInfo();
setting.Partner = model.Partner.Trim();
setting.Key = model.Key.Trim();
setting.Seller = model.Seller.Trim();
setting.AllowRecharge = model.AllowRecharge;
PluginUtils.SavePluginSet(setting);
}
return RedirectToAction("List", "Plugin", new RouteValueDictionary() { { "area", "Admin" }, { "Type", "1" } });
}
}
至於前台我們有3個方面需要去實現,第一個方面是返回調用動作方法,代碼如下:
/// <summary>
/// 返回調用
/// </summary>
public ActionResult Return()
{
SortedDictionary<string, string> sPara = AlipayCore.GetRequestGet();
if (sPara.Count > 0)//判斷是否有帶返回參數
{
bool verifyResult = AlipayNotify.Verify(sPara, Request.QueryString["notify_id"], Request.QueryString["sign"], AlipayConfig.SignType, AlipayConfig.Key, AlipayConfig.Code, AlipayConfig.VeryfyUrl, AlipayConfig.Partner);
if (verifyResult && (Request.QueryString["trade_status"] == "TRADE_FINISHED" || Request.QueryString["trade_status"] == "TRADE_SUCCESS"))//驗證成功
{
int oid = TypeHelper.StringToInt(Request.QueryString["out_trade_no"]);//商戶訂單號
string tradeSN = Request.QueryString["trade_no"];//支付寶交易號
decimal tradeMoney = TypeHelper.StringToDecimal(Request.QueryString["total_fee"]);//交易金額
DateTime tradeTime = TypeHelper.StringToDateTime(Request.QueryString["notify_time"]);//交易時間
OrderInfo orderInfo = Orders.GetOrderByOid(oid);
if (orderInfo.PayMode == 1 && orderInfo.SurplusMoney > 0 && orderInfo.SurplusMoney <= tradeMoney)
{
Orders.PayOrder(oid, OrderState.Confirming, tradeSN);
OrderActions.CreateOrderAction(new OrderActionInfo()
{
Oid = oid,
Uid = orderInfo.Uid,
RealName = "本人",
AdminGid = 1,
AdminGTitle = "非管理員",
ActionType = (int)OrderActionType.Pay,
ActionTime = tradeTime,
ActionDes = "你使用支付寶支付訂單成功,支付寶交易號為:" + tradeSN
});
}
return RedirectToAction("PaySuccess", "Order", new RouteValueDictionary { { "oid", orderInfo.Oid } });
}
else//驗證失敗
{
return new EmptyResult();
}
}
else
{
return new EmptyResult();
}
}
第二個方面是通知調用動作方法,代碼如下:
/// <summary>
/// 通知調用
/// </summary>
public ActionResult Notify()
{
SortedDictionary<string, string> sPara = AlipayCore.GetRequestPost();
if (sPara.Count > 0)//判斷是否有帶返回參數
{
bool verifyResult = AlipayNotify.Verify(sPara, Request.QueryString["notify_id"], Request.QueryString["sign"], AlipayConfig.SignType, AlipayConfig.Key, AlipayConfig.Code, AlipayConfig.VeryfyUrl, AlipayConfig.Partner);
if (verifyResult && (Request.QueryString["trade_status"] == "TRADE_FINISHED" || Request.QueryString["trade_status"] == "TRADE_SUCCESS"))//驗證成功
{
int oid = TypeHelper.StringToInt(Request.QueryString["out_trade_no"]);//商戶訂單號
string tradeSN = Request.QueryString["trade_no"];//支付寶交易號
decimal tradeMoney = TypeHelper.StringToDecimal(Request.QueryString["total_fee"]);//交易金額
DateTime tradeTime = TypeHelper.StringToDateTime(Request.QueryString["gmt_payment"]);//交易時間
OrderInfo orderInfo = Orders.GetOrderByOid(oid);
if (orderInfo.PayMode == 1 && orderInfo.SurplusMoney > 0 && orderInfo.SurplusMoney <= tradeMoney)
{
Orders.PayOrder(oid, OrderState.Confirming, tradeSN);
OrderActions.CreateOrderAction(new OrderActionInfo()
{
Oid = oid,
Uid = orderInfo.Uid,
RealName = "本人",
AdminGid = 1,
AdminGTitle = "非管理員",
ActionType = (int)OrderActionType.Pay,
ActionTime = tradeTime,
ActionDes = "你使用支付寶支付訂單成功,支付寶交易號為:" + tradeSN
});
}
return new EmptyResult();
}
else//驗證失敗
{
return new EmptyResult();
}
}
else
{
return new EmptyResult();
}
}
第三個方面是我們需要提供一個支付寶簡介的視圖文件,這個視圖文件在顧客支付訂單時給其展示一些支付寶的說明信息,此文件有以下幾點要求:
- 文件名必須為"Show.cshtml"。
- 此文件必須位於插件項目的Views文件夾的頂層中。
- 此視圖文件接收類型為OrderInfo的視圖模型對象。
這個視圖文件在BrnShop.Web項目的支付展示視圖文件PayShowModel.cshtml中調用,調用方式如下:
@Html.Partial(Model.ShowView, Model.OrderInfo)
至此支付寶插件開發完成,至於其它類型的插件開發也都是大同小異。
PS:最后附上一張BrnShop開啟NOSQL前后的性能對比圖,以商品詳細頁面為例:
開啟NOSQL前:
開啟NOSQL后:
可見性能提升還是很明顯的。

