重要通知: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后:
可見性能提升還是很明顯的。