From 592aa6e1525f22d9f47985ba04d92311ebcee0d9 Mon Sep 17 00:00:00 2001 From: Varorbc Date: Sat, 4 Jan 2020 18:58:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=93=B6=E8=81=94=E5=95=86?= =?UTF-8?q?=E5=8A=A1=E7=BD=91=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PaySharp.sln | 6 + src/PaySharp.Core/Gateways/GatewayData.cs | 20 +- src/PaySharp.Netpay/Domain/BaseModel.cs | 53 ++++ src/PaySharp.Netpay/Domain/Goods.cs | 71 +++++ src/PaySharp.Netpay/Domain/QueryModel.cs | 25 ++ src/PaySharp.Netpay/Domain/SubOrders.cs | 24 ++ src/PaySharp.Netpay/Domain/UnifiedPayModel.cs | 115 ++++++++ src/PaySharp.Netpay/Merchant.cs | 73 +++++ src/PaySharp.Netpay/NetpayGateway.cs | 102 +++++++ src/PaySharp.Netpay/PaySharp.Netpay.csproj | 18 ++ src/PaySharp.Netpay/PaySharp.snk | Bin 0 -> 596 bytes .../PaySharpConfigExtensions.cs | 60 ++++ src/PaySharp.Netpay/Request/BaseRequest.cs | 29 ++ src/PaySharp.Netpay/Request/QueryRequest.cs | 13 + .../Request/UnifiedPayRequest.cs | 9 + src/PaySharp.Netpay/Response/BaseResponse.cs | 162 +++++++++++ .../Response/NotifyResponse.cs | 257 ++++++++++++++++++ src/PaySharp.Netpay/Response/QueryResponse.cs | 99 +++++++ .../Response/UnifiedPayResponse.cs | 31 +++ .../ServiceCollectionExtensions.cs | 46 ++++ src/PaySharp.Netpay/SubmitProcess.cs | 75 +++++ test/PaySharp.UnitTest/NetpayGatewayTest.cs | 69 +++++ .../PaySharp.UnitTest.csproj | 1 + 23 files changed, 1355 insertions(+), 3 deletions(-) create mode 100644 src/PaySharp.Netpay/Domain/BaseModel.cs create mode 100644 src/PaySharp.Netpay/Domain/Goods.cs create mode 100644 src/PaySharp.Netpay/Domain/QueryModel.cs create mode 100644 src/PaySharp.Netpay/Domain/SubOrders.cs create mode 100644 src/PaySharp.Netpay/Domain/UnifiedPayModel.cs create mode 100644 src/PaySharp.Netpay/Merchant.cs create mode 100644 src/PaySharp.Netpay/NetpayGateway.cs create mode 100644 src/PaySharp.Netpay/PaySharp.Netpay.csproj create mode 100644 src/PaySharp.Netpay/PaySharp.snk create mode 100644 src/PaySharp.Netpay/PaySharpConfigExtensions.cs create mode 100644 src/PaySharp.Netpay/Request/BaseRequest.cs create mode 100644 src/PaySharp.Netpay/Request/QueryRequest.cs create mode 100644 src/PaySharp.Netpay/Request/UnifiedPayRequest.cs create mode 100644 src/PaySharp.Netpay/Response/BaseResponse.cs create mode 100644 src/PaySharp.Netpay/Response/NotifyResponse.cs create mode 100644 src/PaySharp.Netpay/Response/QueryResponse.cs create mode 100644 src/PaySharp.Netpay/Response/UnifiedPayResponse.cs create mode 100644 src/PaySharp.Netpay/ServiceCollectionExtensions.cs create mode 100644 src/PaySharp.Netpay/SubmitProcess.cs create mode 100644 test/PaySharp.UnitTest/NetpayGatewayTest.cs diff --git a/PaySharp.sln b/PaySharp.sln index 73c0462..86f4ff6 100644 --- a/PaySharp.sln +++ b/PaySharp.sln @@ -38,6 +38,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaySharp.Allinpay", "src\PaySharp.Allinpay\PaySharp.Allinpay.csproj", "{5D183F2A-BBB4-4B3E-9951-4584A70ECB60}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PaySharp.Netpay", "src\PaySharp.Netpay\PaySharp.Netpay.csproj", "{1F2BA5BA-6915-4D5C-AD42-55AF4A42FCB2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {5D183F2A-BBB4-4B3E-9951-4584A70ECB60}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D183F2A-BBB4-4B3E-9951-4584A70ECB60}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D183F2A-BBB4-4B3E-9951-4584A70ECB60}.Release|Any CPU.Build.0 = Release|Any CPU + {1F2BA5BA-6915-4D5C-AD42-55AF4A42FCB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F2BA5BA-6915-4D5C-AD42-55AF4A42FCB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F2BA5BA-6915-4D5C-AD42-55AF4A42FCB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F2BA5BA-6915-4D5C-AD42-55AF4A42FCB2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/PaySharp.Core/Gateways/GatewayData.cs b/src/PaySharp.Core/Gateways/GatewayData.cs index 5d22369..ec3d27d 100644 --- a/src/PaySharp.Core/Gateways/GatewayData.cs +++ b/src/PaySharp.Core/Gateways/GatewayData.cs @@ -360,9 +360,23 @@ namespace PaySharp.Core /// public string ToUrl(bool isUrlEncode = true) { - return string.Join("&", - _values - .Select(a => $"{a.Key}={(isUrlEncode ? WebUtility.UrlEncode(a.Value.ToString()) : a.Value.ToString())}")); + var values = _values.Select(a => + { + var value = a.Value.ToString(); + if (a.Value is bool) + { + value = value.ToLower(); + } + + if (isUrlEncode) + { + value = WebUtility.UrlEncode(value); + } + + return $"{a.Key}={value}"; + }); + + return string.Join("&", values); } /// diff --git a/src/PaySharp.Netpay/Domain/BaseModel.cs b/src/PaySharp.Netpay/Domain/BaseModel.cs new file mode 100644 index 0000000..e1c0ceb --- /dev/null +++ b/src/PaySharp.Netpay/Domain/BaseModel.cs @@ -0,0 +1,53 @@ +using System; +using System.ComponentModel.DataAnnotations; +using PaySharp.Core; +using PaySharp.Core.Utils; + +namespace PaySharp.Netpay.Domain +{ + /// + /// 基础模型 + /// + public class BaseModel + { + /// + /// 消息编号 + /// + [StringLength(64, ErrorMessage = "消息编号最大长度为64位")] + public string MsgId { get; set; } = Util.GenerateNonceStr(); + + /// + /// 消息类型 + /// + /// + /// 微信:wx.appPreOrder(存量商户需改为新的msgType) + /// 支付宝:trade.precreate + /// 全民付:qmf.order + /// 银联云闪付(走银联全渠道):uac.appOrder + /// ApplePay支付:applepay.order + /// 查询订单:query + /// + [Required(ErrorMessage = "请设置消息类型")] + [StringLength(64, MinimumLength = 1, ErrorMessage = "消息类型,最小长度为1位,最大长度为64位")] + public string MsgType { get; set; } + + /// + /// 报文请求时间 + /// + [Required(ErrorMessage = "请设置报文请求时间")] + public string RequestTimestamp { get; set; } = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + + /// + /// 商户的交易订单号 + /// + [ReName(Name = "merOrderId")] + [StringLength(32, MinimumLength = 6, ErrorMessage = "商户的交易订单号,最小长度为6位,最大长度为32位")] + [Required(ErrorMessage = "请设置商户的交易订单号")] + public string OutTradeNo { get; set; } + + /// + /// 请求系统预留字段 + /// + public string SrcReserve { get; set; } + } +} diff --git a/src/PaySharp.Netpay/Domain/Goods.cs b/src/PaySharp.Netpay/Domain/Goods.cs new file mode 100644 index 0000000..e12fa26 --- /dev/null +++ b/src/PaySharp.Netpay/Domain/Goods.cs @@ -0,0 +1,71 @@ +using PaySharp.Core; + +namespace PaySharp.Netpay.Domain +{ + /// + /// 商品模型 + /// + public class Goods + { + /// + /// 折扣 + /// + public long Discount { get; set; } + + /// + /// 单位 + /// + public string Unit { get; set; } + + /// + /// 商品编号 + /// + [ReName(Name = "goodsId")] + public string Id { get; set; } + + /// + /// 商品名称 + /// + [ReName(Name = "goodsName")] + public string Name { get; set; } + + /// + /// 商品数量 + /// + public long Quantity { get; set; } + + /// + /// 商品单价(分) + /// + public long Price { get; set; } + + /// + /// 商品分类 + /// + [ReName(Name = "goodsCategory")] + public string Category { get; set; } + + /// + /// 商品说明 + /// + public string Body { get; set; } + + /// + /// 子商户号 + /// + [ReName(Name = "subMerchantId")] + public string SubMchId { get; set; } + + /// + /// 商户子订单号 + /// + [ReName(Name = "merOrderId")] + public string OutTradeNo { get; set; } + + /// + /// 子商户商品总额 + /// + [ReName(Name = "subOrderAmount")] + public int TotalAmount { get; set; } + } +} diff --git a/src/PaySharp.Netpay/Domain/QueryModel.cs b/src/PaySharp.Netpay/Domain/QueryModel.cs new file mode 100644 index 0000000..190c50c --- /dev/null +++ b/src/PaySharp.Netpay/Domain/QueryModel.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using PaySharp.Core; + +namespace PaySharp.Netpay.Domain +{ + public class QueryModel : BaseModel, IValidatableObject + { + /// + /// 交易单号 + /// + [ReName(Name = "targetOrderId")] + public string TradeNo { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrEmpty(OutTradeNo) && string.IsNullOrEmpty(TradeNo)) + { + yield return new ValidationResult("商户订单号和交易单号不能同时为空"); + } + + yield return ValidationResult.Success; + } + } +} diff --git a/src/PaySharp.Netpay/Domain/SubOrders.cs b/src/PaySharp.Netpay/Domain/SubOrders.cs new file mode 100644 index 0000000..28d8310 --- /dev/null +++ b/src/PaySharp.Netpay/Domain/SubOrders.cs @@ -0,0 +1,24 @@ +using PaySharp.Core; + +namespace PaySharp.Netpay.Domain +{ + public class SubOrders + { + /// + /// 子商户号 + /// + [ReName(Name = "mid")] + public string MchId { get; set; } + + /// + /// 商户子订单号 + /// + [ReName(Name = "merOrderId")] + public string OutTradeNo { get; set; } + + /// + /// 子商户分账金额 + /// + public int TotalAmount { get; set; } + } +} diff --git a/src/PaySharp.Netpay/Domain/UnifiedPayModel.cs b/src/PaySharp.Netpay/Domain/UnifiedPayModel.cs new file mode 100644 index 0000000..c233dba --- /dev/null +++ b/src/PaySharp.Netpay/Domain/UnifiedPayModel.cs @@ -0,0 +1,115 @@ +using System; +using System.ComponentModel.DataAnnotations; +using PaySharp.Core; + +namespace PaySharp.Netpay.Domain +{ + public class UnifiedPayModel : BaseModel + { + /// + /// 商品信息 + /// + public Goods Goods { get; set; } + + /// + /// 商户附加数据 + /// + [StringLength(255, ErrorMessage = "商户附加数据最大长度为64位")] + public string AttachedData { get; set; } + + /// + /// 订单过期时间 + /// + public string ExpireTime { get; set; } = DateTime.Now.AddMinutes(30).ToString("yyyy-MM-dd HH:mm:ss"); + + /// + /// 商品标记,用于优惠活动 + /// + [StringLength(32, ErrorMessage = "商品标记最大长度为32位")] + public string GoodsTag { get; set; } + + /// + /// 商品交易单号 + /// 跟goods字段二选一,商品信息通过goods.add接口提前上送 + /// + public string GoodsTradeNo { get; set; } + + /// + /// 商品描述 + /// + [ReName(Name = "orderDesc")] + [StringLength(255, ErrorMessage = "商品描述最大长度为255位")] + public string Body { get; set; } + + /// + /// 订单原始金额,单位分,用于记录前端系统打折前的金额 + /// + public int OriginalAmount { get; set; } + + /// + /// 商品编号 + /// + public string ProductId { get; set; } + + /// + /// 支付总金额,单位为分 + /// + /// + /// 若为true,则:totalAmount = subOrders字段中的所有totalAmount值之和 + /// 加上platformAmount值 = goods中的所有subOrderAmount值之和。 + /// + public int TotalAmount { get; set; } + + /// + /// 分账标记 + /// 暂时只支持微信(WXPay.jsPay)、支付宝(trade.jsPay)支付 和 银联云闪付(uac.appOrder) + /// 若为true,则goods字段和subOrders字段不能同时为空;且secureTransaction字段上送false或不上送。 + /// + public bool DivisionFlag { get; set; } + + /// + /// 平台商户分账金额 + /// + /// 若分账标记传,则分账金额必传 + public int? PlatformAmount { get; set; } + + /// + /// 在传分账标记的情况下,若传子商户号,子商户分账金额必传,即ubOrders每个元素的mid和totalAmount非空 且 mid不超过20个。 + /// 分账方案subOrders里子商户分账总额+platformAmount要与支付总额totalAmount相等 + /// + public SubOrders SubOrders { get; set; } + + /// + /// 交易类型 + /// 微信必传:APP + /// + public string TradeType { get; set; } + + /// + /// 商户用户号 + /// + [ReName(Name = "merchantUserId")] + [StringLength(32, ErrorMessage = "商户用户号最大长度为32位")] + public string MchUserId { get; set; } + + /// + /// 手机号 + /// + [StringLength(11, ErrorMessage = "手机号最大长度为11位")] + public string Mobile { get; set; } + + /// + /// 标识是否是担保交易 + /// 若上送为true,则交易的金额将会被暂缓结算。 + /// 调用担保完成接口后,完成部分金额会在t+1日结算给商户,剩余部分金额退还用户。 + /// 调用担保撤销接口,则全部资金退还给用户。 + /// 30天后 没有主动调用担保完成 且 没有主动调用担保撤销的交易 将会自动按撤销处理。 + /// + public bool SecureTransaction { get; set; } + + /// + /// 是否需要限制信用卡支付 + /// + public bool LimitCreditCard { get; set; } + } +} diff --git a/src/PaySharp.Netpay/Merchant.cs b/src/PaySharp.Netpay/Merchant.cs new file mode 100644 index 0000000..02729d2 --- /dev/null +++ b/src/PaySharp.Netpay/Merchant.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; +using PaySharp.Core; + +namespace PaySharp.Netpay +{ + public class Merchant : IMerchant + { + #region 属性 + + /// + /// 终端号 + /// + [ReName(Name = "tid")] + [Required(ErrorMessage = "请输入终端号")] + [StringLength(32, MinimumLength = 2, ErrorMessage = "终端号最大长度为32位")] + public string AppId { get; set; } + + /// + /// 微信子AppId + /// + /// 只对微信支付有效 + [StringLength(32, ErrorMessage = "微信子AppId最大长度为32位")] + public string SubAppId { get; set; } + + /// + /// 商户号 + /// + [ReName(Name = "mid")] + [Required(ErrorMessage = "请设置商户号")] + [StringLength(32, MinimumLength = 8, ErrorMessage = "商户号最大长度为32位")] + public string MchId { get; set; } + + /// + /// 机构商户号 + /// + [ReName(Name = "instMid")] + [StringLength(32, MinimumLength = 8, ErrorMessage = "机构商户号最大长度为32位")] + public string InstMchId { get; set; } + + /// + /// MD5交易密钥 + /// + [Required(ErrorMessage = "请设置MD5交易密钥")] + [Ignore] + public string Key { get; set; } + + /// + /// 来源 + /// + [ReName(Name = "msgSrc")] + [Required(ErrorMessage = "请设置来源")] + public string Source { get; set; } + + /// + /// 签名类型 + /// + public string SignType => "MD5"; + + /// + /// 返回地址 + /// + [ReName(Name = "showUrl")] + public string ReturnUrl { get; set; } + + /// + /// 网关回发通知URL + /// + [Required(ErrorMessage = "请输入网关回发通知URL")] + public string NotifyUrl { get; set; } + + #endregion + } +} diff --git a/src/PaySharp.Netpay/NetpayGateway.cs b/src/PaySharp.Netpay/NetpayGateway.cs new file mode 100644 index 0000000..6399cd0 --- /dev/null +++ b/src/PaySharp.Netpay/NetpayGateway.cs @@ -0,0 +1,102 @@ +#if NETCOREAPP3_1 +using Microsoft.Extensions.Options; +#endif +using System.Threading.Tasks; +using PaySharp.Core; +using PaySharp.Core.Exceptions; +using PaySharp.Core.Request; +using PaySharp.Core.Utils; +using PaySharp.Netpay.Response; + +namespace PaySharp.Netpay +{ + /// + /// 银联商务网关 + /// + public sealed class NetpayGateway : BaseGateway + { + #region 私有字段 + + private readonly Merchant _merchant; + + #endregion + + #region 构造函数 + + /// + /// 初始化银联商务网关 + /// + /// 商户数据 + public NetpayGateway(Merchant merchant) + : base(merchant) + { + _merchant = merchant; + } + +#if NETCOREAPP3_1 + + /// + /// 初始化银联商务网关 + /// + /// 商户数据 + public NetpayGateway(IOptions merchant) + : this(merchant.Value) + { + } + +#endif + + #endregion + + #region 属性 + + public override string GatewayUrl { get; set; } = "https://qr.chinaums.com"; + + public new NotifyResponse NotifyResponse => (NotifyResponse)base.NotifyResponse; + + protected override bool IsPaySuccess => NotifyResponse.TradeStatus == "TRADE_SUCCESS"; + + protected override bool IsRefundSuccess => NotifyResponse.TradeStatus == "TRADE_REFUND"; + + protected override bool IsCancelSuccess { get; } + + protected override string[] NotifyVerifyParameter => new string[] + { + "tid", "mid", "merOrderId" + }; + + #endregion + + #region 方法 + + protected override async Task ValidateNotifyAsync() + { + base.NotifyResponse = await GatewayData.ToObjectAsync(StringCase.Camel); + base.NotifyResponse.Raw = GatewayData.Raw; + + if (!SubmitProcess.CheckSign(GatewayData, _merchant.Key, NotifyResponse.Sign)) + { + throw new GatewayException("签名不一致"); + } + + return true; + } + + protected override void WriteSuccessFlag() + { + HttpUtil.Write("SUCCESS"); + } + + protected override void WriteFailureFlag() + { + HttpUtil.Write("FAILED"); + } + + public override TResponse Execute(Request request) + { + return SubmitProcess.Execute(_merchant, request, GatewayUrl); + } + + #endregion + } +} diff --git a/src/PaySharp.Netpay/PaySharp.Netpay.csproj b/src/PaySharp.Netpay/PaySharp.Netpay.csproj new file mode 100644 index 0000000..eb63fc2 --- /dev/null +++ b/src/PaySharp.Netpay/PaySharp.Netpay.csproj @@ -0,0 +1,18 @@ + + + + + + net45;netcoreapp3.1 + PaySharp.Netpay + 通联收银宝 + dotnetcore;pay;Netpay; + + + + + + + + + diff --git a/src/PaySharp.Netpay/PaySharp.snk b/src/PaySharp.Netpay/PaySharp.snk new file mode 100644 index 0000000000000000000000000000000000000000..48386da1050fdcd58f476e0aeccd4b0a780b97c6 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096|$o3B?MFd;~f+;h+)JI?eJa^-Tl(y9( z4Hn9B()I2=y`>=xLR=8YO5EPFaKGzi=8qJUDfSQ4$In@wpy5wZ(XsFC_(L|R%D z;dwEEEgi4j{f+|nnUxf)m0n9t_r__0*#2=&^KC7)>ANEJCo5iVO4O~4sN}D*nT>N}ki$n1V3!OA%Y@oLE#C%b#>H7B_P0Toq@-Hx_lTU$*@8XWr#h26|LsTMw1Z)Wd)pZujEinn!< zx>8STvUN-a{bfnrS-RU%P8I`+WabWN4WE?39$+S(K action) + { + if (action != null) + { + var merchant = new Merchant(); + action(merchant); + gateways.Add(new NetpayGateway(merchant)); + } + + return gateways; + } + + public static IGateways RegisterNetpay(this IGateways gateways) + { + var merchants = (List)ConfigurationManager.GetSection("paySharp/Netpays"); + if (merchants == null) + { + return gateways; + } + + foreach (var item in merchants) + { + var netpayGateway = new NetpayGateway(new Merchant + { + AppId = item["appId"].ToString(), + SubAppId = item["subAppId"].ToString(), + MchId = item["mchId"].ToString(), + InstMchId = item["instMchId"].ToString(), + Key = item["key"].ToString(), + Source = item["source"].ToString(), + NotifyUrl = item["notifyUrl"].ToString(), + ReturnUrl = item["returnUrl"].ToString() + }); + + var gatewayUrl = item["gatewayUrl"].ToString(); + if (!string.IsNullOrEmpty(gatewayUrl)) + { + netpayGateway.GatewayUrl = gatewayUrl; + } + + gateways.Add(netpayGateway); + } + + return gateways; + } + } +} + +#endif diff --git a/src/PaySharp.Netpay/Request/BaseRequest.cs b/src/PaySharp.Netpay/Request/BaseRequest.cs new file mode 100644 index 0000000..2bae4e0 --- /dev/null +++ b/src/PaySharp.Netpay/Request/BaseRequest.cs @@ -0,0 +1,29 @@ +using PaySharp.Core.Request; +using PaySharp.Core.Response; +using PaySharp.Core.Utils; + +namespace PaySharp.Netpay.Request +{ + public class BaseRequest : Request where TResponse : IResponse + { + public BaseRequest() + { + RequestUrl = "/netpay-route-server/api/"; + } + + public override void AddGatewayData(TModel model) + { + base.AddGatewayData(model); + + GatewayData.Add(model, StringCase.Camel); + } + + internal virtual void Execute(Merchant merchant) + { + if (!string.IsNullOrEmpty(NotifyUrl)) + { + GatewayData.Add("notifyUrl", NotifyUrl); + } + } + } +} diff --git a/src/PaySharp.Netpay/Request/QueryRequest.cs b/src/PaySharp.Netpay/Request/QueryRequest.cs new file mode 100644 index 0000000..c634555 --- /dev/null +++ b/src/PaySharp.Netpay/Request/QueryRequest.cs @@ -0,0 +1,13 @@ +using PaySharp.Netpay.Domain; +using PaySharp.Netpay.Response; + +namespace PaySharp.Netpay.Request +{ + public class QueryRequest : BaseRequest + { + internal override void Execute(Merchant merchant) + { + GatewayData.Remove("notifyUrl"); + } + } +} diff --git a/src/PaySharp.Netpay/Request/UnifiedPayRequest.cs b/src/PaySharp.Netpay/Request/UnifiedPayRequest.cs new file mode 100644 index 0000000..2f74c6a --- /dev/null +++ b/src/PaySharp.Netpay/Request/UnifiedPayRequest.cs @@ -0,0 +1,9 @@ +using PaySharp.Netpay.Domain; +using PaySharp.Netpay.Response; + +namespace PaySharp.Netpay.Request +{ + public class UnifiedPayRequest : BaseRequest + { + } +} diff --git a/src/PaySharp.Netpay/Response/BaseResponse.cs b/src/PaySharp.Netpay/Response/BaseResponse.cs new file mode 100644 index 0000000..4f5b9d5 --- /dev/null +++ b/src/PaySharp.Netpay/Response/BaseResponse.cs @@ -0,0 +1,162 @@ +using PaySharp.Core; +using PaySharp.Core.Request; +using PaySharp.Core.Response; + +namespace PaySharp.Netpay.Response +{ + public abstract class BaseResponse : IResponse + { + /// + /// 平台错误码 + /// + public string ErrCode { get; set; } + + /// + /// 平台错误信息 + /// + public string ErrMsg { get; set; } + + /// + /// 消息编号 + /// + public string MsgId { get; set; } + + /// + /// 来源 + /// + [ReName(Name = "msgSrc")] + public string Source { get; set; } + + /// + /// 消息类型 + /// + public string MsgType { get; set; } + + /// + /// 请求系统预留字段 + /// + public string SrcReserve { get; set; } + + /// + /// 报文响应时间 + /// + public string ResponseTimestamp { get; set; } + + /// + /// 商户名称 + /// + [ReName(Name = "merName")] + public string MchName { get; set; } + + /// + /// 商户号 + /// + [ReName(Name = "mid")] + public string MchId { get; set; } + + /// + /// 商户订单号 + /// + [ReName(Name = "merOrderId")] + public string OutTradeNo { get; set; } + + /// + /// 终端号 + /// + [ReName(Name = "tid")] + public string AppId { get; set; } + + /// + /// 平台流水号 + /// + [ReName(Name = "seqId")] + public string TradeNo { get; set; } + + /// + /// 清分ID,如果来源方传了bankRefId就等于bankRefId,否则等于seqId + /// + public string SettleRefId { get; set; } + + /// + /// 交易状态 + /// NEW_ORDER 新订单 + /// UNKNOWN 不明确的交易状态 + /// TRADE_CLOSED 在指定时间段内未支付时关闭的交易;在交易完成撤销成功时关闭的交易;支付失败的交易。 + /// WAIT_BUYER_PAY  交易创建,等待买家付款。 + /// TRADE_SUCCESS 支付成功 + /// TRADE_REFUND 订单转入退货流程 + /// + [ReName(Name = "status")] + public string TradeStatus { get; set; } + + /// + /// 支付总金额 + /// + public int TotalAmount { get; set; } + + /// + /// 连接系统 + /// + public string ConnectSys { get; set; } + + /// + /// 渠道订单号 + /// + [ReName(Name = "targetOrderId")] + public string ChannelTradeNo { get; set; } + + /// + /// 渠道代码 + /// Alipay 1.0 支付宝1.0协议 + /// Alipay 2.0 支付宝2.0协议 + /// WXPay 微信 + /// YQB 壹钱包 + /// QMF 全民付远程快捷 + /// UnionPay 银联钱包 + /// BaiDu 百度钱包 + /// JD 京东钱包 + /// SF 顺丰顺手付 + /// COMM 交通银行 + /// BestPay 翼支付 + /// ACP 银联全渠道立码付 + /// NetPayBills 银商网付平台账单模块 + /// NetPayGtwy 银商网付平台网关模块 + /// QmfWebPay POS通插件WEB版 + /// UAC 银联全渠道 + /// + [ReName(Name = "targetSys")] + public string ChannelCode { get; set; } + + /// + /// 渠道状态 + /// + [ReName(Name = "targetStatus")] + public string ChannelStatus { get; set; } + + /// + /// 渠道商户号 + /// + [ReName(Name = "targetMid")] + public string ChannelMchId { get; set; } + + /// + /// 营销联盟优惠金额 + /// + [ReName(Name = "yxlmAmount")] + public int MarketingAffiliateAmount { get; set; } + + /// + /// 签名 + /// + public string Sign { get; set; } + + /// + /// 原始值 + /// + public string Raw { get; set; } + + internal GatewayData GatewayData { get; set; } + + internal abstract void Execute(Merchant merchant, Request request) where TResponse : IResponse; + } +} diff --git a/src/PaySharp.Netpay/Response/NotifyResponse.cs b/src/PaySharp.Netpay/Response/NotifyResponse.cs new file mode 100644 index 0000000..723474f --- /dev/null +++ b/src/PaySharp.Netpay/Response/NotifyResponse.cs @@ -0,0 +1,257 @@ +using System; +using PaySharp.Core; +using PaySharp.Core.Response; + +namespace PaySharp.Netpay.Response +{ + public class NotifyResponse : IResponse + { + /// + /// 商户号 + /// + [ReName(Name = "mid")] + public string MchId { get; set; } + + /// + /// 终端号 + /// + [ReName(Name = "tid")] + public string AppId { get; set; } + + /// + /// 机构商户号 + /// + [ReName(Name = "instMid")] + public string InstMchId { get; set; } + + /// + /// 商户附加数据 + /// + public string AttachedData { get; set; } + + /// + /// 银行卡号 + /// + public string BankCardNo { get; set; } + + /// + /// 支付渠道列表 + /// + public string BillFunds { get; set; } + + /// + /// 支付渠道描述 + /// + public string BillFundsDesc { get; set; } + + /// + /// 买家编号 + /// + public string BuyerId { get; set; } + + /// + /// 子买家编号 + /// + public string SubBuyerId { get; set; } + + /// + /// 买家用户名 + /// + public string BuyerUsername { get; set; } + + /// + /// 买家付款的金额 + /// + public int BuyerPayAmount { get; set; } + + /// + /// 支付总金额 + /// + public int TotalAmount { get; set; } + + /// + /// 发票金额 + /// + public int InvoiceAmount { get; set; } + + /// + /// 实收金额 + /// + public int ReceiptAmount { get; set; } + + /// + /// 退款金额 + /// + public int RefundAmount { get; set; } + + /// + /// 退款说明 + /// + public string RefundDesc { get; set; } + + /// + /// 渠道退款号 + /// + [ReName(Name = "refundTargetOrderId")] + public string ChannelRefundNo { get; set; } + + /// + /// 退款时间 + /// + [ReName(Name = "refundPayTime")] + public DateTime? RefundTime { get; set; } + + /// + /// 退款结算日期 + /// + [ReName(Name = "refundPayTime")] + public DateTime? RefundSettleDate { get; set; } + + /// + /// 商户订单号 + /// + [ReName(Name = "merOrderId")] + public string OutTradeNo { get; set; } + + /// + /// 支付时间 + /// + public DateTime PayTime { get; set; } + + /// + /// 检索参考号,用在银联体系交易中 + /// + public string RefId { get; set; } + + /// + /// 平台流水号 + /// + [ReName(Name = "seqId")] + public string TradeNo { get; set; } + + /// + /// 结算日期 + /// + public DateTime SettleDate { get; set; } + + /// + /// 交易状态 + /// NEW_ORDER 新订单 + /// UNKNOWN 不明确的交易状态 + /// TRADE_CLOSED 在指定时间段内未支付时关闭的交易;在交易完成撤销成功时关闭的交易;支付失败的交易。 + /// WAIT_BUYER_PAY  交易创建,等待买家付款。 + /// TRADE_SUCCESS 支付成功 + /// TRADE_REFUND 订单转入退货流程 + /// + [ReName(Name = "status")] + public string TradeStatus { get; set; } + + /// + /// 连接系统 + /// + public string ConnectSys { get; set; } + + /// + /// 渠道订单号 + /// + [ReName(Name = "targetOrderId")] + public string ChannelTradeNo { get; set; } + + /// + /// 渠道代码 + /// Alipay 1.0 支付宝1.0协议 + /// Alipay 2.0 支付宝2.0协议 + /// WXPay 微信 + /// YQB 壹钱包 + /// QMF 全民付远程快捷 + /// UnionPay 银联钱包 + /// BaiDu 百度钱包 + /// JD 京东钱包 + /// SF 顺丰顺手付 + /// COMM 交通银行 + /// BestPay 翼支付 + /// ACP 银联全渠道立码付 + /// NetPayBills 银商网付平台账单模块 + /// NetPayGtwy 银商网付平台网关模块 + /// QmfWebPay POS通插件WEB版 + /// UAC 银联全渠道 + /// + [ReName(Name = "targetSys")] + public string ChannelCode { get; set; } + + /// + /// 营销联盟优惠金额 + /// + [ReName(Name = "yxlmAmount")] + public int MarketingAffiliateAmount { get; set; } + + /// + /// 签名 + /// + public string Sign { get; set; } + + /// + /// 商户出资优惠金额 + /// + public int CouponMerchantContribute { get; set; } + + /// + /// 其他出资优惠金额 + /// + public int CouponOtherContribute { get; set; } + + /// + /// 微信活动编号 + /// + public string ActivityIds { get; set; } + + /// + /// 订单详情 + /// + [ReName(Name = "orderDesc")] + public string Body { get; set; } + + /// + /// 创建时间 + /// + public string CreateTime { get; set; } + + /// + /// 商户UUID + /// + [ReName(Name = "mchntUuid")] + public string MchUuid { get; set; } + + /// + /// 商户所属分支机构代码 + /// + public string SubInst { get; set; } + + /// + /// 退货外部订单号 + /// + public string RefundExtOrderId { get; set; } + + /// + /// 商品交易单号 + /// + public string GoodsTradeNo { get; set; } + + /// + /// 外部订单号 + /// + public string ExtOrderId { get; set; } + + /// + /// 担保交易状态 + /// + public string SecureStatus { get; set; } + + /// + /// 担保完成金额 + /// + public string CompleteAmount { get; set; } + + public string Raw { get; set; } + } +} diff --git a/src/PaySharp.Netpay/Response/QueryResponse.cs b/src/PaySharp.Netpay/Response/QueryResponse.cs new file mode 100644 index 0000000..470ab6d --- /dev/null +++ b/src/PaySharp.Netpay/Response/QueryResponse.cs @@ -0,0 +1,99 @@ +using System; +using PaySharp.Core; +using PaySharp.Core.Request; + +namespace PaySharp.Netpay.Response +{ + public class QueryResponse : BaseResponse + { + /// + /// 机构商户号 + /// + [ReName(Name = "instMid")] + public string InstMchId { get; set; } + + /// + /// 检索参考号,用在银联体系交易中 + /// + public string RefId { get; set; } + + /// + /// 买家编号 + /// + public string BuyerId { get; set; } + + /// + /// 子买家编号 + /// + public string SubBuyerId { get; set; } + + /// + /// 买家用户名 + /// + public string BuyerUsername { get; set; } + + /// + /// 买家付款的金额 + /// + public int BuyerPayAmount { get; set; } + + /// + /// 银行卡号 + /// + public string BankCardNo { get; set; } + + /// + /// 银行信息 + /// + public string BankInfo { get; set; } + + /// + /// 支付渠道列表 + /// + public string BillFunds { get; set; } + + /// + /// 支付渠道描述 + /// + public string BillFundsDesc { get; set; } + + /// + /// 优惠金额 + /// + public int CouponAmount { get; set; } + + /// + /// 发票金额 + /// + public int InvoiceAmount { get; set; } + + /// + /// 实收金额 + /// + public int ReceiptAmount { get; set; } + + /// + /// 退款金额 + /// + public int RefundAmount { get; set; } + + /// + /// 支付时间 + /// + public DateTime PayTime { get; set; } + + /// + /// 结算日期 + /// + public DateTime SettleDate { get; set; } + + /// + /// 微信活动编号 + /// + public string ActivityIds { get; set; } + + internal override void Execute(Merchant merchant, Request request) + { + } + } +} diff --git a/src/PaySharp.Netpay/Response/UnifiedPayResponse.cs b/src/PaySharp.Netpay/Response/UnifiedPayResponse.cs new file mode 100644 index 0000000..2c05c69 --- /dev/null +++ b/src/PaySharp.Netpay/Response/UnifiedPayResponse.cs @@ -0,0 +1,31 @@ +using PaySharp.Core.Request; + +namespace PaySharp.Netpay.Response +{ + public class UnifiedPayResponse : BaseResponse + { + /// + /// JSAPI支付用的请求报文,带有签名信息 + /// + public string JsPayRequest { get; set; } + + /// + /// APP支付用的请求报文,带有签名信息 + /// + public string AppPayRequest { get; set; } + + /// + /// 支付ID,用于APP支付和公众号支付 + /// + public string PrepayId { get; set; } + + /// + /// 支付二维码,内容为URL,由终端转换成二维码显示 + /// + public string QrCode { get; set; } + + internal override void Execute(Merchant merchant, Request request) + { + } + } +} diff --git a/src/PaySharp.Netpay/ServiceCollectionExtensions.cs b/src/PaySharp.Netpay/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ee7c27e --- /dev/null +++ b/src/PaySharp.Netpay/ServiceCollectionExtensions.cs @@ -0,0 +1,46 @@ +#if NETCOREAPP3_1 +using System; +using Microsoft.Extensions.Configuration; +using PaySharp.Netpay; +using PaySharp.Core; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IGateways UseNetpay(this IGateways gateways, Action action) + { + if (action != null) + { + var merchant = new Merchant(); + action(merchant); + gateways.Add(new NetpayGateway(merchant)); + } + + return gateways; + } + + public static IGateways UseNetpay(this IGateways gateways, IConfiguration configuration) + { + var merchants = configuration.GetSection("PaySharp:Netpays").Get(); + if (merchants != null) + { + for (var i = 0; i < merchants.Length; i++) + { + var netpayGateway = new NetpayGateway(merchants[i]); + var gatewayUrl = configuration.GetSection($"PaySharp:Netpays:{i}:GatewayUrl").Value; + if (!string.IsNullOrEmpty(gatewayUrl)) + { + netpayGateway.GatewayUrl = gatewayUrl; + } + + gateways.Add(netpayGateway); + } + } + + return gateways; + } + } +} + +#endif diff --git a/src/PaySharp.Netpay/SubmitProcess.cs b/src/PaySharp.Netpay/SubmitProcess.cs new file mode 100644 index 0000000..9d9dae1 --- /dev/null +++ b/src/PaySharp.Netpay/SubmitProcess.cs @@ -0,0 +1,75 @@ +using PaySharp.Core; +using PaySharp.Core.Exceptions; +using PaySharp.Core.Request; +using PaySharp.Core.Response; +using PaySharp.Core.Utils; +using PaySharp.Netpay.Request; +using PaySharp.Netpay.Response; + +namespace PaySharp.Netpay +{ + internal static class SubmitProcess + { + private static string _gatewayUrl; + + internal static TResponse Execute(Merchant merchant, Request request, string gatewayUrl = null) where TResponse : IResponse + { + AddMerchant(merchant, request, gatewayUrl); + + var sign = BuildSign(request.GatewayData, merchant.Key); + request.GatewayData.Add("sign", sign); + + var result = HttpUtil.Post(request.RequestUrl, request.GatewayData.ToJson()); + + var gatewayData = new GatewayData(); + gatewayData.FromJson(result); + + var baseResponse = (BaseResponse)(object)gatewayData.ToObject(StringCase.Camel); + baseResponse.Raw = result; + baseResponse.GatewayData = gatewayData; + if (baseResponse.ErrCode == "SUCCESS") + { + sign = gatewayData.GetStringValue("sign"); + + if (!string.IsNullOrEmpty(sign) && !CheckSign(gatewayData, merchant.Key, sign)) + { + throw new GatewayException("签名验证失败"); + } + + baseResponse.Execute(merchant, request); + } + + return (TResponse)(object)baseResponse; + } + + private static void AddMerchant(Merchant merchant, Request request, string gatewayUrl) where TResponse : IResponse + { + if (!string.IsNullOrEmpty(gatewayUrl)) + { + _gatewayUrl = gatewayUrl; + } + + if (!request.RequestUrl.StartsWith("http")) + { + request.RequestUrl = _gatewayUrl + request.RequestUrl; + } + request.GatewayData.Add(merchant, StringCase.Camel); + ((BaseRequest)request).Execute(merchant); + } + + internal static string BuildSign(GatewayData gatewayData, string key) + { + gatewayData.Remove("sign"); + + var data = gatewayData.ToUrl(false) + key; + var sign = EncryptUtil.MD5(data); + + return sign; + } + + internal static bool CheckSign(GatewayData gatewayData, string key, string sign) + { + return BuildSign(gatewayData, key) == sign; + } + } +} diff --git a/test/PaySharp.UnitTest/NetpayGatewayTest.cs b/test/PaySharp.UnitTest/NetpayGatewayTest.cs new file mode 100644 index 0000000..0fd9a49 --- /dev/null +++ b/test/PaySharp.UnitTest/NetpayGatewayTest.cs @@ -0,0 +1,69 @@ +using System; +using PaySharp.Netpay; +using PaySharp.Netpay.Domain; +using PaySharp.Netpay.Request; +using Xunit; +using Xunit.Abstractions; + +namespace PaySharp.UnitTest +{ + public class NetpayGatewayTest + { + private readonly ITestOutputHelper _output; + private readonly NetpayGateway _netpayGateway; + private readonly Merchant _merchant; + private string OutTradeNo => DateTime.Now.ToString("3194yyyyMMddhhmmss"); + + public NetpayGatewayTest(ITestOutputHelper output) + { + _output = output; + + _merchant = new Merchant + { + AppId = "00000001", + MchId = "898310148160568", + Key = "fcAmtnx7MwismjWNhNKdHC44mNXtnEQeJkRrhKJwyrW2ysRR", + Source = "WWW.TEST.COM", + NotifyUrl = "http://localhost:61337/Notify" + }; + + _netpayGateway = new NetpayGateway(_merchant) + { + GatewayUrl = "https://qr-test2.chinaums.com" + }; + } + + [Fact] + public void TestUnifiedPay() + { + var request = new UnifiedPayRequest(); + request.AddGatewayData(new UnifiedPayModel + { + MsgType = "trade.precreate", + Body = "银联商务统一支付测试", + TotalAmount = 1, + OriginalAmount = 1, + OutTradeNo = OutTradeNo + }); + + var response = _netpayGateway.Execute(request); + + _output.WriteLine(response.QrCode); + Assert.NotNull(response.QrCode); + } + + [Fact] + public void TestQuery() + { + var request = new QueryRequest(); + request.AddGatewayData(new QueryModel() + { + MsgType = "query", + OutTradeNo = "319420200104045313" + }); + + var response = _netpayGateway.Execute(request); + Assert.Equal(1, response.TotalAmount); + } + } +} diff --git a/test/PaySharp.UnitTest/PaySharp.UnitTest.csproj b/test/PaySharp.UnitTest/PaySharp.UnitTest.csproj index fec7229..b461185 100644 --- a/test/PaySharp.UnitTest/PaySharp.UnitTest.csproj +++ b/test/PaySharp.UnitTest/PaySharp.UnitTest.csproj @@ -26,6 +26,7 @@ +