feat: 添加微信应用模块,支持微信支付及微信小程序对接(功能产品已上线验证)

This commit is contained in:
橙子
2023-09-03 11:22:19 +08:00
parent efee87e4c5
commit c2ca0b1f29
21 changed files with 1264 additions and 0 deletions

View File

@@ -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<SmsAliyunOptions>(App.Configuration.GetSection("SmsAliyunOptions"));
services.Configure<CachingConnOptions>(App.Configuration.GetSection("CachingConnOptions"));
services.Configure<WeChatOptions>(App.Configuration.GetSection("WeChatOptions"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View File

@@ -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
{
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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,//:支付失败(其他原因,如银行返回失败)
}
}

View File

@@ -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
{
/// <summary>
/// 获取用户openid
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<Code2SessionResponse> Code2SessionAsync(Code2SessionInput input);
/// <summary>
/// 获取不限制的小程序码
/// <param name="scene"></param>
/// <returns></returns>
/// </summary>
Task<string> GetQRCodeAsync(string scene, string page, EnvVersionEnum unlimitedQRCodeEnum = EnvVersionEnum.release);
/// <summary>
/// 支付预支付id
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<JsApiResponse> JsApiAsync(JsApiInput input);
/// <summary>
/// 支付的回调接口
/// </summary>
/// <param name="reponse"></param>
/// <returns></returns>
PayNoticeResult PayNotice(PayNoticeReponse reponse);
/// <summary>
/// 发送聚合消息
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task SendUniformMessageAsync(UniformMessageInput input);
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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 {
/// <summary>
/// 商品描述
/// </summary>
public string description { get; set; }
/// <summary>
/// 商户订单号
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public AmountItemRequest amount { get; set; }
/// <summary>
/// 支付者
/// </summary>
public PayerItemRequest payer { get; set; }
}
public class JsApiRequest
{
/// <summary>
/// 应用id
/// </summary>
public string appid { get; set; }
/// <summary>
/// 商户id
/// </summary>
public string mchid { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string description { get; set; }
/// <summary>
/// 商户订单号
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 回调通知地址
/// </summary>
public string notify_url { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public AmountItemRequest amount { get; set; }
/// <summary>
/// 支付者
/// </summary>
public PayerItemRequest payer { get; set; }
}
public class AmountItemRequest
{
/// <summary>
/// 总金额
/// </summary>
public int total { get; set; }
}
public class PayerItemRequest
{
public string openid { get; set; }
}
public class JsApiResponse
{
/// <summary>
/// 预支付id
/// </summary>
public string prepay_id { get; set; }
}
}

View File

@@ -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
{
/// <summary>
/// 接收的结果
/// </summary>
public class PayNoticeReponse
{
/// <summary>
/// 通知的唯一ID
/// </summary>
public string id { get; set; }
/// <summary>
/// 通知的资源数据类型支付成功通知为encrypt-resource
/// </summary>
public string create_time { get; set; }
/// <summary>
/// 通知的资源数据类型支付成功通知为encrypt-resource
/// </summary>
public string event_type { get; set; }
/// <summary>
/// 通知的资源数据类型支付成功通知为encrypt-resource
/// </summary>
public string resource_type { get; set; }
/// <summary>
/// 回调摘要
/// </summary>
public string summary { get; set; }
/// <summary>
/// 数据
/// </summary>
public Resource resource { get; set; }
}
/// <summary>
/// 通知的数据
/// </summary>
public class Resource
{
/// <summary>
/// 加密算法类型AEAD_AES_256_GCM
/// </summary>
public string algorithm { get; set; }
/// <summary>
/// 数据密文
/// </summary>
public string ciphertext { get; set; }
/// <summary>
/// 附加数据
/// </summary>
public string associated_data { get; set; }
/// <summary>
/// 原始回调类型为transaction
/// </summary>
public string original_type { get; set; }
/// <summary>
/// 随机串
/// </summary>
public string nonce { get; set; }
}
/// <summary>
/// 解密出来的结果
/// </summary>
public class PayNoticeResult
{
/// <summary>
/// 微信支付系统生成的订单号。
/// </summary>
public string transaction_id { get; set; }
/// <summary>
/// 订单金额信息
/// </summary>
public Amount amount { get; set; }
/// <summary>
/// 商户号
/// </summary>
public string mchid { get; set; }
/// <summary>
/*
交易状态,枚举值:
SUCCESS支付成功
REFUND转入退款
NOTPAY未支付
CLOSED已关闭
REVOKED已撤销付款码支付
USERPAYING用户支付中付款码支付
PAYERROR支付失败(其他原因,如银行返回失败)
*/
/// </summary>
public string trade_state { get; set; }
/// <summary>
/// 银行类型,采用字符串类型的银行标识。
/// </summary>
public string bank_type { get; set; }
/// <summary>
/// 优惠功能,享受优惠时返回该字段
/// </summary>
public List<PromotionDetail> promotion_detail { get; set; }
/// <summary>
/// 支付完成时间
/// </summary>
public string success_time { get; set; }
/// <summary>
/// 支付者信息
/// </summary>
public Payer payer { get; set; }
/// <summary>
/// 商户系统内部订单号只能是数字、大小写字母_-*且在同一个商户号下唯一。
/// 特殊规则最小字符长度为6
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 应用ID
/// </summary>
public string appid { get; set; }
/// <summary>
/// 交易状态描述
/// </summary>
public string trade_state_desc { get; set; }
/// <summary>
/* 交易类型,枚举值:
JSAPI公众号支付
NATIVE扫码支付
APPAPP支付
MICROPAY付款码支付
MWEBH5支付
FACEPAY刷脸支付
*/
/// </summary>
public string trade_type { get; set; }
/// <summary>
/*
* 银行类型,采用字符串类型的银行标识。
*/
/// </summary>
public string attach { get; set; }
/// <summary>
/// 支付场景信息描述
/// </summary>
public SceneInfo scene_info { get; set; }
}
public class Amount
{
/// <summary>
/// 用户支付金额,单位为分。
/// </summary>
public int payer_total { get; set; }
/// <summary>
/// 订单总金额,单位为分。
/// </summary>
public int total { get; set; }
/// <summary>
/// CNY人民币境内商户号仅支持人民币。
/// </summary>
public string currency { get; set; }
/// <summary>
/// 用户支付币种
/// </summary>
public string payer_currency { get; set; }
}
public class GoodsDetail
{
/// <summary>
/// 商品备注
/// </summary>
public string goods_remark { get; set; }
/// <summary>
/// 商品编码
/// </summary>
public int quantity { get; set; }
/// <summary>
/// 商品优惠金额
/// </summary>
public int discount_amount { get; set; }
/// <summary>
/// 商品编码
/// </summary>
public string goods_id { get; set; }
/// <summary>
/// 商品单价
/// </summary>
public int unit_price { get; set; }
}
public class PromotionDetail
{
/// <summary>
/// 单品列表
/// </summary>
public int amount { get; set; }
/// <summary>
/// 微信出资
/// </summary>
public int wechatpay_contribute { get; set; }
/// <summary>
/// 券ID
/// </summary>
public string coupon_id { get; set; }
public string scope { get; set; }
public int merchant_contribute { get; set; }
/// <summary>
/// 优惠名称
/// </summary>
public string name { get; set; }
/// <summary>
/// 其他出资
/// </summary>
public int other_contribute { get; set; }
/// <summary>
/// currency
/// </summary>
public string currency { get; set; }
/// <summary>
/// 活动ID
/// </summary>
public string stock_id { get; set; }
/// <summary>
/// 单品列表
/// </summary>
public List<GoodsDetail> goods_detail { get; set; }
}
public class Payer
{
/// <summary>
/// 用户标识
/// </summary>
public string openid { get; set; }
}
public class SceneInfo
{
/// <summary>
/// 商户端设备号
/// </summary>
public string device_id { get; set; }
}
}

View File

@@ -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
{
/// <summary>
///用户openid可以是小程序的openid也可以是mp_template_msg.appid对应的公众号的openid
/// </summary>
public string touser { get; set; }
/// <summary>
/// 小程序消息模板
/// </summary>
public WeappTemplateMsg weapp_template_msg { get; set; } = new WeappTemplateMsg();
/// <summary>
/// 公众号模板
/// </summary>
public MpTemplateMsg mp_template_msg { get; set; } = new MpTemplateMsg();
}
public class UniformMessageInput
{
/// <summary>
///用户openid可以是小程序的openid也可以是mp_template_msg.appid对应的公众号的openid
/// </summary>
public string touser { get; set; }
/// <summary>
/// 小程序消息模板
/// </summary>
public WeappTemplateMsg? weapp_template_msg { get; set; }
/// <summary>
/// 公众号模板
/// </summary>
public MpTemplateMsg? mp_template_msg { get; set; }
}
public class UniformMessageResponse : IErrorObjct
{
public int errcode { get; set; }
public string errmsg { get; set; }
}
/// <summary>
/// 小程序消息
/// </summary>
public class WeappTemplateMsg
{
/// <summary>
/// 模板id
/// </summary>
public string template_id { get; set; }
/// <summary>
/// 小程序页面
/// </summary>
public string page { get; set; }
/// <summary>
/// 小程序模板消息formid
/// </summary>
public string form_id { get; set; }
/// <summary>
/// 小程序模板放大关键词
/// </summary>
public string emphasis_keyword { get; set; }
/// <summary>
/// 模板数据
/// </summary>
public string data { get; set; }
}
/// <summary>
/// 公众号消息通知
/// </summary>
public class MpTemplateMsg
{
/// <summary>
/// 公众号appid要求与小程序有绑定且同主体
/// </summary>
public string appid { get; set; }
/// <summary>
/// 公众号模板id
/// </summary>
public string template_id { get; set; }
/// <summary>
///公众号模板消息所要跳转的url
/// </summary>
public string url { get; set; }
/// <summary>
/// 公众号模板消息所要跳转的小程序,小程序的必须与公众号具有绑定关系
/// </summary>
public Miniprogram miniprogram { get; set; }
/// <summary>
/// 公众号模板消息的数据
/// </summary>
public Dictionary<string, keyValueItem> data { get; set; }
}
/// <summary>
/// 小程序跳转
/// </summary>
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; }
}
}

View File

@@ -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
{
/// <summary>
/// 正式版本
/// </summary>
[Description("正式版本")]
release,
/// <summary>
/// 体验版本
/// </summary>
[Description("体验版本")]
trial,
/// <summary>
/// 开发版本
/// </summary>
[Description("开发版本")]
develop
}
}

View File

@@ -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<WeChatOptions> options)
{
_options = options.Value;
}
public async Task<string> GetTokenAsync()
{
var token = await this.GetAccessToken();
return token.access_token;
}
/// <summary>
/// 获取微信AccessToken
/// </summary>
public async Task<AccessTokenResponse> 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<AccessTokenResponse>();
return responseBody;
}
}
}
}

View File

@@ -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<string> GetTokenAsync();
}
}

View File

@@ -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<WeChatOptions> options, RedisCacheClient cacheManager)
{
_options = options.Value;
_cacheManager = cacheManager;
}
public async Task<string> 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;
}
/// <summary>
/// 获取微信AccessToken
/// </summary>
public async Task<AccessTokenResponse> 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<AccessTokenResponse>();
if (string.IsNullOrEmpty(responseBody?.access_token))
{
throw new WeChatException($"获取accessToken异常返回结果【{await response.Content.ReadAsStringAsync()}】");
}
return responseBody;
}
}
}
}

View File

@@ -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)
{
}
}
}

View File

@@ -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();
}
/// <summary>
/// 效验请求是否成功
/// </summary>
/// <param name="response"></param>
/// <returns></returns>
public static void ValidateSuccess(this IErrorObjct response)
{
if (response.errcode != 0)
{
throw new WeChatException(response.errmsg);
}
}
}
}

View File

@@ -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<WeChatOptions> options, IWeChatToken weChatToken)
{
_options = options.Value;
_weChatToken = weChatToken;
}
/// <summary>
/// 获取用户openid
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<Code2SessionResponse> 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<Code2SessionResponse>();
responseBody.ValidateSuccess();
return responseBody;
}
}
/// <summary>
/// 支付预支付id描述必填
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<JsApiResponse> JsApiAsync(JsApiInput input)
{
string url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
var req = input.Adapt<JsApiRequest>();
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<JsApiResponse>();
return responseBody;
}
}
/// <summary>
/// 支付通知回调
/// </summary>
/// <returns></returns>
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<PayNoticeResult>(result);
return output;
}
/// <summary>
/// 获取不限制的小程序码
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public async Task<string> 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;
}
}
/// <summary>
/// 小程序推送订阅消息
/// </summary>
/// <returns></returns>
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<UniformMessageRequest>();
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<UniformMessageResponse>();
responseBody.ValidateSuccess();
}
}
/// <summary>
/// AEAD_AES_256_GCM解密算法用于解开支付回调的通知
/// </summary>
/// <param name="ciphertextBase64">需要base64</param>
/// <param name="associatedDataBase64">需要base64</param>
/// <param name="nonceBase64">需要base64</param>
/// <returns></returns>
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;
}
}
}

View File

@@ -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; }
/// <summary>
/// 公众号
/// </summary>
public OfficialAccountsOptions OfficialAccounts { get; set; }
}
public class OfficialAccountsOptions {
public string AppId { get; set; }
public string TemplateId { get; set; }
}
}

View File

@@ -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<HttpResponseMessage> 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<string> 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));
}
}
}

View File

@@ -16,6 +16,7 @@
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="2.0.23" />
<PackageReference Include="CSRedisCore" Version="3.8.670" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta15" />
</ItemGroup>