From c2ca0b1f295b2e63c364e1c0b63942951efaeb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com> Date: Sun, 3 Sep 2023 11:22:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=E5=8F=8A=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=AF=B9=E6=8E=A5=EF=BC=88=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E4=BA=A7=E5=93=81=E5=B7=B2=E4=B8=8A=E7=BA=BF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Yi.Furion.Net6/Yi.Framework.Module/Startup.cs | 3 + .../WeChat/Abstract/IErrorObjct.cs | 12 + .../WeChat/Abstract/IHasErrcode.cs | 13 + .../WeChat/Abstract/IHasErrmsg.cs | 13 + .../WeChat/Enums/PayTradeStateEnum.cs | 19 ++ .../WeChat/IWeChatManager.cs | 47 +++ .../WeChat/Model/AccessTokenHttpModel.cs | 22 ++ .../WeChat/Model/Code2SessionHttpModel.cs | 36 +++ .../WeChat/Model/JsApiHttpModel.cs | 98 ++++++ .../WeChat/Model/PayNoticeHttpModel.cs | 288 ++++++++++++++++++ .../WeChat/Model/UniformMessageHttpModel.cs | 142 +++++++++ .../WeChat/Model/UnlimitedQRCodeHttpModel.cs | 48 +++ .../WeChat/Token/DefaultWeChatToken.cs | 53 ++++ .../WeChat/Token/IWeChatToken.cs | 14 + .../WeChat/Token/RedisWeChatToken.cs | 72 +++++ .../WeChat/WeChatException.cs | 34 +++ .../WeChat/WeChatExtensions.cs | 48 +++ .../WeChat/WeChatManager.cs | 200 ++++++++++++ .../WeChat/WeChatOptions.cs | 32 ++ .../WeChat/WeChatPayHttpHandler.cs | 69 +++++ .../Yi.Framework.Module.csproj | 1 + 21 files changed, 1264 insertions(+) create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IErrorObjct.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrcode.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrmsg.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Enums/PayTradeStateEnum.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/IWeChatManager.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/AccessTokenHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/Code2SessionHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/JsApiHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/PayNoticeHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UniformMessageHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UnlimitedQRCodeHttpModel.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/DefaultWeChatToken.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/IWeChatToken.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/RedisWeChatToken.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatException.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatExtensions.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatManager.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatOptions.cs create mode 100644 Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatPayHttpHandler.cs diff --git a/Yi.Furion.Net6/Yi.Framework.Module/Startup.cs b/Yi.Furion.Net6/Yi.Framework.Module/Startup.cs index b8ede490..f4eb3b01 100644 --- a/Yi.Furion.Net6/Yi.Framework.Module/Startup.cs +++ b/Yi.Furion.Net6/Yi.Framework.Module/Startup.cs @@ -8,6 +8,7 @@ using Yi.Framework.Infrastructure.Sqlsugar; using Yi.Framework.Module.Caching; using Yi.Framework.Module.ImageSharp.HeiCaptcha; using Yi.Framework.Module.Sms.Aliyun; +using Yi.Framework.Module.WeChat; namespace Yi.Framework.Module; @@ -26,6 +27,8 @@ public class Startup : AppStartup services.Configure(App.Configuration.GetSection("SmsAliyunOptions")); services.Configure(App.Configuration.GetSection("CachingConnOptions")); + + services.Configure(App.Configuration.GetSection("WeChatOptions")); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IErrorObjct.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IErrorObjct.cs new file mode 100644 index 00000000..74d5c5ac --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IErrorObjct.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Abstract +{ + public interface IErrorObjct: IHasErrcode, IHasErrmsg + { + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrcode.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrcode.cs new file mode 100644 index 00000000..c0627a9e --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrcode.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Abstract +{ + public interface IHasErrcode + { + public int errcode { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrmsg.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrmsg.cs new file mode 100644 index 00000000..405bd0ad --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Abstract/IHasErrmsg.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Abstract +{ + public interface IHasErrmsg + { + string errmsg { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Enums/PayTradeStateEnum.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Enums/PayTradeStateEnum.cs new file mode 100644 index 00000000..9fa665d4 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Enums/PayTradeStateEnum.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Enums +{ + public enum PayTradeStateEnum + { + SUCCESS,//:支付成功 + REFUND,//:转入退款 + NOTPAY,//:未支付 + CLOSED,//:已关闭 + REVOKED,//:已撤销(付款码支付) + USERPAYING,//:用户支付中(付款码支付) + PAYERROR,//:支付失败(其他原因,如银行返回失败) + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/IWeChatManager.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/IWeChatManager.cs new file mode 100644 index 00000000..efd23ce1 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/IWeChatManager.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.Module.WeChat.Model; + +namespace Yi.Framework.Module.WeChat +{ + public interface IWeChatManager + { + /// + /// 获取用户openid + /// + /// + /// + Task Code2SessionAsync(Code2SessionInput input); + + /// + /// 获取不限制的小程序码 + /// + /// + /// + Task GetQRCodeAsync(string scene, string page, EnvVersionEnum unlimitedQRCodeEnum = EnvVersionEnum.release); + + /// + /// 支付预支付id + /// + /// + /// + Task JsApiAsync(JsApiInput input); + + /// + /// 支付的回调接口 + /// + /// + /// + PayNoticeResult PayNotice(PayNoticeReponse reponse); + + /// + /// 发送聚合消息 + /// + /// + /// + Task SendUniformMessageAsync(UniformMessageInput input); + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/AccessTokenHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/AccessTokenHttpModel.cs new file mode 100644 index 00000000..4b8ecb57 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/AccessTokenHttpModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Model +{ + public class AccessTokenResponse + { + public string access_token { get; set; } + + public int expires_in { get; set; } + } + + public class AccessTokenRequest + { + public string grant_type { get; set; } + public string appid { get; set; } + public string secret { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/Code2SessionHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/Code2SessionHttpModel.cs new file mode 100644 index 00000000..9e07cdae --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/Code2SessionHttpModel.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.Module.WeChat.Abstract; + +namespace Yi.Framework.Module.WeChat.Model +{ + public class Code2SessionResponse: IErrorObjct + { + public string openid { get; set; } + public string session_key { get; set; } + public string unionid { get; set; } + public int errcode { get; set; } + public string errmsg { get; set; } + } + + public class Code2SessionRequest + { + public string appid { get; set; } + public string secret { get; set; } + public string js_code { get; set; } + public string grant_type => "authorization_code"; + } + + public class Code2SessionInput + { + public Code2SessionInput(string js_code) + { + + this.js_code=js_code; + } + public string js_code { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/JsApiHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/JsApiHttpModel.cs new file mode 100644 index 00000000..3a51d95c --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/JsApiHttpModel.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Model +{ + public class JsApiInput { + + /// + /// 商品描述 + /// + public string description { get; set; } + + /// + /// 商户订单号 + /// + public string out_trade_no { get; set; } + + + /// + /// 订单金额 + /// + public AmountItemRequest amount { get; set; } + + + /// + /// 支付者 + /// + public PayerItemRequest payer { get; set; } + } + + public class JsApiRequest + { + /// + /// 应用id + /// + public string appid { get; set; } + + /// + /// 商户id + /// + public string mchid { get; set; } + + + /// + /// 商品描述 + /// + public string description { get; set; } + + /// + /// 商户订单号 + /// + public string out_trade_no { get; set; } + + + /// + /// 回调通知地址 + /// + public string notify_url { get; set; } + + + /// + /// 订单金额 + /// + public AmountItemRequest amount { get; set; } + + + /// + /// 支付者 + /// + public PayerItemRequest payer { get; set; } + } + + public class AmountItemRequest + { + /// + /// 总金额 + /// + public int total { get; set; } + } + + + public class PayerItemRequest + { + public string openid { get; set; } + } + + + public class JsApiResponse + { + /// + /// 预支付id + /// + public string prepay_id { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/PayNoticeHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/PayNoticeHttpModel.cs new file mode 100644 index 00000000..5b2222e7 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/PayNoticeHttpModel.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Furion; + +namespace Yi.Framework.Module.WeChat.Model +{ + /// + /// 接收的结果 + /// + public class PayNoticeReponse + { + + /// + /// 通知的唯一ID + /// + public string id { get; set; } + + /// + /// 通知的资源数据类型,支付成功通知为encrypt-resource + /// + public string create_time { get; set; } + + /// + /// 通知的资源数据类型,支付成功通知为encrypt-resource + /// + public string event_type { get; set; } + + /// + /// 通知的资源数据类型,支付成功通知为encrypt-resource + /// + public string resource_type { get; set; } + + /// + /// 回调摘要 + /// + public string summary { get; set; } + + /// + /// 数据 + /// + public Resource resource { get; set; } + } + + /// + /// 通知的数据 + /// + public class Resource + { + /// + /// 加密算法类型:AEAD_AES_256_GCM + /// + public string algorithm { get; set; } + + /// + /// 数据密文 + /// + public string ciphertext { get; set; } + + /// + /// 附加数据 + /// + public string associated_data { get; set; } + + /// + /// 原始回调类型,为transaction + /// + public string original_type { get; set; } + + /// + /// 随机串 + /// + public string nonce { get; set; } + } + + + + /// + /// 解密出来的结果 + /// + public class PayNoticeResult + { + /// + /// 微信支付系统生成的订单号。 + /// + public string transaction_id { get; set; } + + /// + /// 订单金额信息 + /// + public Amount amount { get; set; } + + /// + /// 商户号 + /// + public string mchid { get; set; } + + /// + /* + 交易状态,枚举值: + SUCCESS:支付成功 + REFUND:转入退款 + NOTPAY:未支付 + CLOSED:已关闭 + REVOKED:已撤销(付款码支付) + USERPAYING:用户支付中(付款码支付) + PAYERROR:支付失败(其他原因,如银行返回失败) + */ + /// + public string trade_state { get; set; } + + /// + /// 银行类型,采用字符串类型的银行标识。 + /// + public string bank_type { get; set; } + + /// + /// 优惠功能,享受优惠时返回该字段 + /// + public List promotion_detail { get; set; } + + /// + /// 支付完成时间 + /// + public string success_time { get; set; } + + /// + /// 支付者信息 + /// + public Payer payer { get; set; } + + /// + /// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。 + /// 特殊规则:最小字符长度为6 + /// + public string out_trade_no { get; set; } + + /// + /// 应用ID + /// + public string appid { get; set; } + + /// + /// 交易状态描述 + /// + public string trade_state_desc { get; set; } + + /// + /* 交易类型,枚举值: + JSAPI:公众号支付 + NATIVE:扫码支付 + APP:APP支付 + MICROPAY:付款码支付 + MWEB:H5支付 + FACEPAY:刷脸支付 + */ + /// + public string trade_type { get; set; } + + /// + /* + * 银行类型,采用字符串类型的银行标识。 + */ + /// + public string attach { get; set; } + + + /// + /// 支付场景信息描述 + /// + public SceneInfo scene_info { get; set; } + + } + public class Amount + { + /// + /// 用户支付金额,单位为分。 + /// + public int payer_total { get; set; } + /// + /// 订单总金额,单位为分。 + /// + public int total { get; set; } + + /// + /// CNY:人民币,境内商户号仅支持人民币。 + /// + public string currency { get; set; } + + /// + /// 用户支付币种 + /// + public string payer_currency { get; set; } + } + + public class GoodsDetail + { + /// + /// 商品备注 + /// + public string goods_remark { get; set; } + + /// + /// 商品编码 + /// + public int quantity { get; set; } + + /// + /// 商品优惠金额 + /// + public int discount_amount { get; set; } + + /// + /// 商品编码 + /// + public string goods_id { get; set; } + + /// + /// 商品单价 + /// + public int unit_price { get; set; } + } + + public class PromotionDetail + { + /// + /// 单品列表 + /// + public int amount { get; set; } + + /// + /// 微信出资 + /// + public int wechatpay_contribute { get; set; } + + /// + /// 券ID + /// + public string coupon_id { get; set; } + public string scope { get; set; } + public int merchant_contribute { get; set; } + + /// + /// 优惠名称 + /// + public string name { get; set; } + + /// + /// 其他出资 + /// + public int other_contribute { get; set; } + + /// + /// currency + /// + public string currency { get; set; } + + /// + /// 活动ID + /// + public string stock_id { get; set; } + + /// + /// 单品列表 + /// + public List goods_detail { get; set; } + } + + public class Payer + { + /// + /// 用户标识 + /// + public string openid { get; set; } + } + + public class SceneInfo + { + /// + /// 商户端设备号 + /// + public string device_id { get; set; } + } + + +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UniformMessageHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UniformMessageHttpModel.cs new file mode 100644 index 00000000..3f942c20 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UniformMessageHttpModel.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yi.Framework.Module.WeChat.Abstract; + +namespace Yi.Framework.Module.WeChat.Model +{ + public class UniformMessageRequest + { + /// + ///用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid + /// + public string touser { get; set; } + + + /// + /// 小程序消息模板 + /// + public WeappTemplateMsg weapp_template_msg { get; set; } = new WeappTemplateMsg(); + + /// + /// 公众号模板 + /// + public MpTemplateMsg mp_template_msg { get; set; } = new MpTemplateMsg(); + } + + + public class UniformMessageInput + { + + /// + ///用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid + /// + public string touser { get; set; } + + + /// + /// 小程序消息模板 + /// + public WeappTemplateMsg? weapp_template_msg { get; set; } + + /// + /// 公众号模板 + /// + public MpTemplateMsg? mp_template_msg { get; set; } + } + + public class UniformMessageResponse : IErrorObjct + { + public int errcode { get; set; } + public string errmsg { get; set; } + } + + /// + /// 小程序消息 + /// + public class WeappTemplateMsg + { + + /// + /// 模板id + /// + public string template_id { get; set; } + + /// + /// 小程序页面 + /// + public string page { get; set; } + + /// + /// 小程序模板消息formid + /// + public string form_id { get; set; } + + /// + /// 小程序模板放大关键词 + /// + public string emphasis_keyword { get; set; } + + /// + /// 模板数据 + /// + public string data { get; set; } + } + + + /// + /// 公众号消息通知 + /// + public class MpTemplateMsg + { + /// + /// 公众号appid,要求与小程序有绑定且同主体 + /// + public string appid { get; set; } + + /// + /// 公众号模板id + /// + public string template_id { get; set; } + + + /// + ///公众号模板消息所要跳转的url + /// + public string url { get; set; } + + /// + /// 公众号模板消息所要跳转的小程序,小程序的必须与公众号具有绑定关系 + /// + public Miniprogram miniprogram { get; set; } + + /// + /// 公众号模板消息的数据 + /// + public Dictionary data { get; set; } + + + } + + /// + /// 小程序跳转 + /// + public class Miniprogram + { + + public string appid { get; set; } + public string pagepath { get; set; } + } + + + + public class keyValueItem + { + public string value { get; set; } + public string color { get; set; } + + + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UnlimitedQRCodeHttpModel.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UnlimitedQRCodeHttpModel.cs new file mode 100644 index 00000000..4d68cd77 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Model/UnlimitedQRCodeHttpModel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Model +{ + public class UnlimitedQRCodeReponse + { + + } + + public class UnlimitedQRCodeRequest + { + public UnlimitedQRCodeRequest(EnvVersionEnum unlimitedQRCodeEnum) { + env_version = unlimitedQRCodeEnum.ToString(); + } + + public bool check_path { get; set; } = false; + public string page { get; set; } + + public string scene { get; set; } + public string env_version { get; set; } + } + + public enum EnvVersionEnum + { + /// + /// 正式版本 + /// + [Description("正式版本")] + release, + + /// + /// 体验版本 + /// + [Description("体验版本")] + trial, + + /// + /// 开发版本 + /// + [Description("开发版本")] + develop + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/DefaultWeChatToken.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/DefaultWeChatToken.cs new file mode 100644 index 00000000..acc77674 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/DefaultWeChatToken.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; +using Furion.DependencyInjection; +using Microsoft.Extensions.Options; +using Yi.Framework.Module.WeChat; +using Yi.Framework.Module.WeChat.Extensions; +using Yi.Framework.Module.WeChat.Model; +using Yi.Framework.Module.WeChat.Token; + +namespace Zeng.CarMovingAssistant.Application.CarMoving.Services.Impl +{ + public class DefaultWeChatToken : IWeChatToken,ITransient + { + private WeChatOptions _options; + public DefaultWeChatToken(IOptions options) + { + _options = options.Value; + } + public async Task GetTokenAsync() + { + var token = await this.GetAccessToken(); + return token.access_token; + } + + /// + /// 获取微信AccessToken + /// + public async Task GetAccessToken() + { + string url = "https://api.weixin.qq.com/cgi-bin/token"; + var req = new AccessTokenRequest(); + req.appid = _options.AppId; + req.secret = _options.AppSecret; + req.grant_type = "client_credential"; + using (HttpClient httpClient = new HttpClient()) + { + string queryString = req.ToQueryString(); + var builder = new UriBuilder(url); + builder.Query = queryString; + HttpResponseMessage response = await httpClient.GetAsync(builder.ToString()); + + response.EnsureSuccessStatusCode(); + + var responseBody = await response.Content.ReadFromJsonAsync(); + return responseBody; + } + } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/IWeChatToken.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/IWeChatToken.cs new file mode 100644 index 00000000..8760dc49 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/IWeChatToken.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat.Token +{ + public interface IWeChatToken + { + public Task GetTokenAsync(); + } + +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/RedisWeChatToken.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/RedisWeChatToken.cs new file mode 100644 index 00000000..a91810ef --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/Token/RedisWeChatToken.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; +using Furion.DependencyInjection; +using Microsoft.Extensions.Options; +using Yi.Framework.Module.Caching; +using Yi.Framework.Module.WeChat.Extensions; +using Yi.Framework.Module.WeChat.Model; + +namespace Yi.Framework.Module.WeChat.Token +{ + public class RedisWeChatToken : IWeChatToken + { + private WeChatOptions _options; + private RedisCacheClient _cacheManager; + private const string RedisAccessTokenKey = $"WeChat:AccessTokenKey"; + + public RedisWeChatToken(IOptions options, RedisCacheClient cacheManager) + { + _options = options.Value; + _cacheManager = cacheManager; + } + public async Task GetTokenAsync() + { + string accessToken; + + if (_cacheManager.Client.Exists(RedisAccessTokenKey)) + { + accessToken = _cacheManager.Client.Get(RedisAccessTokenKey); + } + else + { + var response = await this.GetAccessToken(); + accessToken = response.access_token; + _cacheManager.Client.Set(RedisAccessTokenKey, accessToken, TimeSpan.FromHours(1)); + } + + return accessToken; + } + + /// + /// 获取微信AccessToken + /// + public async Task GetAccessToken() + { + string url = "https://api.weixin.qq.com/cgi-bin/token"; + var req = new AccessTokenRequest(); + req.appid = _options.AppId; + req.secret = _options.AppSecret; + req.grant_type = "client_credential"; + using (HttpClient httpClient = new HttpClient()) + { + string queryString = req.ToQueryString(); + var builder = new UriBuilder(url); + builder.Query = queryString; + HttpResponseMessage response = await httpClient.GetAsync(builder.ToString()); + + response.EnsureSuccessStatusCode(); + + var responseBody = await response.Content.ReadFromJsonAsync(); + if (string.IsNullOrEmpty(responseBody?.access_token)) + { + throw new WeChatException($"获取accessToken异常,返回结果【{await response.Content.ReadAsStringAsync()}】"); + } + return responseBody; + } + } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatException.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatException.cs new file mode 100644 index 00000000..44e62228 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatException.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat +{ + public class WeChatException : Exception + { + public override string Message + { + get + { + // 加上前缀 + return "微信Api异常: " + base.Message; + } + } + + public WeChatException() + { + } + + public WeChatException(string message) + : base(message) + { + } + + public WeChatException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatExtensions.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatExtensions.cs new file mode 100644 index 00000000..9112284d --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Furion; +using Microsoft.Extensions.DependencyInjection; +using Yi.Framework.Module.WeChat.Abstract; + +namespace Yi.Framework.Module.WeChat.Extensions +{ + public static class WeChatExtensions + { + public static string ToQueryString(this object model) + { + var properties = model.GetType().GetProperties(); + var query = HttpUtility.ParseQueryString(string.Empty); + + foreach (var property in properties) + { + var value = property.GetValue(model); + if (value != null) + { + query[property.Name] = value.ToString(); + } + } + + return "?" + query.ToString(); + } + + /// + /// 效验请求是否成功 + /// + /// + /// + public static void ValidateSuccess(this IErrorObjct response) + { + + if (response.errcode != 0) + { + throw new WeChatException(response.errmsg); + } + } + + + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatManager.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatManager.cs new file mode 100644 index 00000000..e8d23844 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatManager.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Security.Policy; +using System.Text; +using System.Threading.Tasks; +using Furion.DependencyInjection; +using Furion.JsonSerialization; +using Mapster; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using Yi.Framework.Module.WeChat.Extensions; +using Yi.Framework.Module.WeChat.Model; +using Yi.Framework.Module.WeChat.Token; + +namespace Yi.Framework.Module.WeChat +{ + public class WeChatManager : IWeChatManager, ISingleton + { + private WeChatOptions _options; + private IWeChatToken _weChatToken; + public WeChatManager(IOptions options, IWeChatToken weChatToken) + { + _options = options.Value; + _weChatToken = weChatToken; + } + + /// + /// 获取用户openid + /// + /// + /// + public async Task Code2SessionAsync(Code2SessionInput input) + { + string url = "https://api.weixin.qq.com/sns/jscode2session"; + var req = new Code2SessionRequest(); + req.js_code = input.js_code; + req.secret = _options.AppSecret; + req.appid = _options.AppId; + + using (HttpClient httpClient = new HttpClient()) + { + string queryString = req.ToQueryString(); + var builder = new UriBuilder(url); + builder.Query = queryString; + HttpResponseMessage response = await httpClient.GetAsync(builder.ToString()); + var responseBody = await response.Content.ReadFromJsonAsync(); + + responseBody.ValidateSuccess(); + + return responseBody; + } + } + + /// + /// 支付预支付id,描述必填 + /// + /// + /// + public async Task JsApiAsync(JsApiInput input) + { + string url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; + + var req = input.Adapt(); + req.mchid = _options.Mchid; + req.notify_url = _options.NotifyUrl; + req.appid = _options.AppId; + + using (HttpClient httpClient = new HttpClient(new WeChatPayHttpHandler(_options.Mchid, _options.MerchantSerialNo))) + {// 设置Accept头 + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + // 设置User-Agent头 + httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"); + JsonContent jsonContent = JsonContent.Create(req); + HttpResponseMessage response = await httpClient.PostAsync(url, jsonContent); + var data = await response.Content.ReadAsStringAsync(); + response.EnsureSuccessStatusCode(); + var responseBody = await response.Content.ReadFromJsonAsync(); + return responseBody; + } + } + + /// + /// 支付通知回调 + /// + /// + public PayNoticeResult PayNotice(PayNoticeReponse reponse) + { + var data = reponse.resource; + var result = AEAD_AES_256_GCM(data.associated_data, data.nonce, data.ciphertext, _options.PaySecretKey); + var output = JSON.Deserialize(result); + return output; + } + + + /// + /// 获取不限制的小程序码 + /// + /// + /// + public async Task GetQRCodeAsync(string scene, string page, EnvVersionEnum unlimitedQRCodeEnum = EnvVersionEnum.release) + { + string url = $"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={await _weChatToken.GetTokenAsync()}"; + var req = new UnlimitedQRCodeRequest(unlimitedQRCodeEnum); + req.scene = scene; + req.page = page; + req.env_version = unlimitedQRCodeEnum.ToString(); + using (HttpClient httpClient = new HttpClient()) + { + StringContent message = new StringContent(System.Text.Json.JsonSerializer.Serialize(req)); + + HttpResponseMessage response = await httpClient.PostAsync(url, message); + + response.EnsureSuccessStatusCode(); + + var stream = await response.Content.ReadAsStreamAsync(); + var result = ConvertStreamToBase64(stream); + return result; + } + } + + /// + /// 小程序推送订阅消息 + /// + /// + public async Task SendUniformMessageAsync(UniformMessageInput input) + { + string url = $"https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token={await _weChatToken.GetTokenAsync()}"; + var req = input.Adapt(); + req.mp_template_msg.template_id = _options.OfficialAccounts.TemplateId; + req.mp_template_msg.appid = _options.OfficialAccounts.AppId; + req.mp_template_msg.miniprogram.appid = _options.AppId; + + using (HttpClient httpClient = new HttpClient()) + { + var body =new StringContent(JSON.Serialize(req)); + HttpResponseMessage response = await httpClient.PostAsync(url, body); + var responseBody = await response.Content.ReadFromJsonAsync(); + + responseBody.ValidateSuccess(); + } + + } + + + /// + /// AEAD_AES_256_GCM解密算法,用于解开支付回调的通知 + /// + /// 需要base64 + /// 需要base64 + /// 需要base64 + /// + public static string AEAD_AES_256_GCM(string associatedData, string nonce, string ciphertext, string secretKey) + { + try + { + GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine()); + AeadParameters aeadParameters = new AeadParameters( + new KeyParameter(Encoding.UTF8.GetBytes(secretKey)), + 128, + Encoding.UTF8.GetBytes(nonce), + Encoding.UTF8.GetBytes(associatedData)); + gcmBlockCipher.Init(false, aeadParameters); + + byte[] data = Convert.FromBase64String(ciphertext); + byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)]; + int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0); + gcmBlockCipher.DoFinal(plaintext, length); + return Encoding.UTF8.GetString(plaintext); + } + catch (Exception ex) + { + throw new WeChatException("支付回调解密错误", ex); + } + } + + private string ConvertStreamToBase64(Stream stream) + { + // 将Stream对象转换为字节数组 + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, (int)stream.Length); + + // 将字节数组转换为Base64字符串 + string base64String = Convert.ToBase64String(buffer); + + return base64String; + } + + + + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatOptions.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatOptions.cs new file mode 100644 index 00000000..462f1bb7 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatOptions.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Yi.Framework.Module.WeChat +{ + public class WeChatOptions + { + public string AppId { get; set; } + public string AppSecret { get; set; } + + public string PaySecretKey { get; set; } + + public string Mchid { get; set; } + + public string NotifyUrl { get; set; } + + public string MerchantSerialNo { get; set; } + + /// + /// 公众号 + /// + public OfficialAccountsOptions OfficialAccounts { get; set; } + } + + public class OfficialAccountsOptions { + public string AppId { get; set; } + public string TemplateId { get; set; } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatPayHttpHandler.cs b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatPayHttpHandler.cs new file mode 100644 index 00000000..cd05d958 --- /dev/null +++ b/Yi.Furion.Net6/Yi.Framework.Module/WeChat/WeChatPayHttpHandler.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Org.BouncyCastle.Asn1.Ocsp; + +namespace Yi.Framework.Module.WeChat +{ + public class WeChatPayHttpHandler : DelegatingHandler + { + private readonly string merchantId; + private readonly string serialNo; + + public WeChatPayHttpHandler(string merchantId, string merchantSerialNo) + { + InnerHandler = new HttpClientHandler(); + + this.merchantId = merchantId; + this.serialNo = merchantSerialNo; + } + + protected async override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + var auth = await BuildAuthAsync(request); + string value = $"WECHATPAY2-SHA256-RSA2048 {auth}"; + request.Headers.Add("Authorization", value); + + return await base.SendAsync(request, cancellationToken); + } + + protected async Task BuildAuthAsync(HttpRequestMessage request) + { + string method = request.Method.ToString(); + string body = ""; + if (method == "POST" || method == "PUT" || method == "PATCH") + { + var content = request.Content; + body = await content.ReadAsStringAsync(); + } + + string uri = request.RequestUri.PathAndQuery; + var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + string nonce = Path.GetRandomFileName(); + + string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n"; + string signature = Sign(message); + return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\""; + } + + protected string Sign(string message) + { + // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- + // 亦不包括结尾的-----END PRIVATE KEY----- + string privateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/7sZqymy7XbgW\r\noZWzgS7Ok4LqPDT05kVrnqSTOeckWNSz8x2o/VHB7UXvQIqOyroNPgOkqXB6Bq59\r\nSWF422uCcwWItZMxdELQChEU9bnd3ia7U4i8gwMGFJOGn2J75CCLa6+IhDwFoC3G\r\nvm7aWH11PSuJd8jLYS4azNsKwJwfAFHbCKqhlHir2qCnFZXabSGnm6obmIUMjCxy\r\ncfnhrPXY8eg/QomyI12wlyO+ZpjWogibI0rleu/zey7z+XhGMcl8qBHjtguN3Sgv\r\nGMGLnm8osjGhY/da1V/IXTsw6/CyG5IP3e0Unwzo9PJwx5nq/zmvQC+uRr1AgcBZ\r\nilKkPwsNAgMBAAECggEAEMltiUGTKQAVbcVMNpsB4Qd918bUSucpAzSo6EeUM9Wh\r\nJOwKmBEv6Wo7R6W5eKu6ghX+c5RuRf33nPWiFNP8Hzi4LzDSYuzsOw3mWJL1YrZf\r\nZNr1hqdeyFVcYdXm4zccsZUFkUcfiM5tsohNYcODlZF4EVnssf0Z7zYjolkeToet\r\nzgU6mQvzQ6zbGeYVOn0A+/+LiCJeZRW+bifQpK4m8JqSD9CSS44VJkWe63r8176o\r\nYXmc5QjxVyUrnJPGgqDZFgc93BGVycJzEDG43QUaCCPdXgXru3Ywsz7pzgVaJHEs\r\nMD4dy9GpZnSKKqD7aAq9G/5/LXCJz5erWd+f20Kn8QKBgQDpLKqtrNxFwIjE8U1+\r\ndQwGlmL3/EjqXNCeQScB6dSAEn8ueWd7JrOyjst2rIx0AD07764K5Uc8xrRBDQg5\r\nYnug5bmGlefIp5CkCtB01Th0wMfgAOTVEtXaaq+6uBfPcSBBEDghTPD5c6oZvPj7\r\nA6ig39fcoUKr9V84VISsYewOAwKBgQDSuJfXhGp5UpJnd49SJSVIZg4FNQVaZfIQ\r\nbhxlRwokAsNaziJryta3Q51afGswr0rj40kEwCogJSlrO9OhktBA74aTuyl3c2s0\r\n4iXzzYjbRBnzN6nAgAHWDMStznAUqyDNzyGvd6uy++erisGq08Ugo0yr10GOKU39\r\nUj4z+a59rwKBgQCiWc5Q9J2+F0tjTNv3I4oHACjSn58pRwyeU6DUTTn/HmHdOvyZ\r\nG55cwd3auFNm5U+9bqmQvok2QOf6rxc91Vtc8PaXRcLHzBwCi+EOp/MSH7RLPHQY\r\nA3BRDp1idZFmh0683o0maosSNL2IBDKbm7WKpbCH1uQ0FLmC4B4sZFXWfwKBgCDx\r\npxuUoijRlf4DHS8Ui52kBvEddvbJFW0oKdxTnOxAWlZp/8umbKc+NO2eogt8fFLg\r\nh9vsRym7ZZxUQCP0lgZw7DNQgY0hSFN+P7y8F3dgUEZMH4fu+1qBqIYbzj4M+xXy\r\nGiwao4daBsA0805HyXvuy9/ZyW/2WTEPmJX7pSIVAoGBAJpZSxjgVnKPDNPNOh0H\r\ndns7IcT8iaFUaoYu/1edWCxW9RWQ9Ml66qFETl7NpMMmilNVmOBUW5hseQ38IgbZ\r\nyt+4DRm9knYCHZCDI46pNugLigQwAkDWsKTxvbK/HwmfQY91cX3tnH2VhPIVsIwf\r\nv5mx7h9s2iAX0f1ThUw3jVfN"; + byte[] keyData = Convert.FromBase64String(privateKey); + + var rsa = RSA.Create(); + //适用该方法的版本https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.asymmetricalgorithm.importpkcs8privatekey?view=net-7.0 + rsa.ImportPkcs8PrivateKey(keyData, out _); + rsa.ImportPkcs8PrivateKey(keyData, out _); + byte[] data = System.Text.Encoding.UTF8.GetBytes(message); + return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); + } + } +} diff --git a/Yi.Furion.Net6/Yi.Framework.Module/Yi.Framework.Module.csproj b/Yi.Furion.Net6/Yi.Framework.Module/Yi.Framework.Module.csproj index da591219..4410c44a 100644 --- a/Yi.Furion.Net6/Yi.Framework.Module/Yi.Framework.Module.csproj +++ b/Yi.Furion.Net6/Yi.Framework.Module/Yi.Framework.Module.csproj @@ -16,6 +16,7 @@ +