重要通知: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;elsereturn 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;//合作者身份IDstring 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.1string 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;//合作者身份IDprivate 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后:
可见性能提升还是很明显的。