diff --git a/SKIT.FlurlHttpClient.Wechat.sln b/SKIT.FlurlHttpClient.Wechat.sln index dc818d0c..73cb450e 100644 --- a/SKIT.FlurlHttpClient.Wechat.sln +++ b/SKIT.FlurlHttpClient.Wechat.sln @@ -18,7 +18,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Ads", "src\SKIT.FlurlHttpClient.Wechat.Ads\SKIT.FlurlHttpClient.Wechat.Ads.csproj", "{7F155EFB-152F-4798-9984-99102B21D2F8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Ads", "src\SKIT.FlurlHttpClient.Wechat.Ads\SKIT.FlurlHttpClient.Wechat.Ads.csproj", "{7F155EFB-152F-4798-9984-99102B21D2F8}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C95AF531-CF44-44AA-AC90-F4DF9F941674}" EndProject @@ -32,7 +32,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Ads.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj", "{561E0BFB-7817-41FE-BAF2-D78817679AC1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Ads.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj", "{561E0BFB-7817-41FE-BAF2-D78817679AC1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{35C901ED-C234-4A91-9561-AD89B3BB788D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5", "samples\SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5\SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.csproj", "{D1B321C9-3004-4645-A78D-A85C152062FA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -56,6 +60,10 @@ Global {CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.Build.0 = Release|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.Build.0 = Release|Any CPU {A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -72,14 +80,14 @@ Global {DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.Build.0 = Release|Any CPU - {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.Build.0 = Release|Any CPU {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Release|Any CPU.Build.0 = Release|Any CPU + {D1B321C9-3004-4645-A78D-A85C152062FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1B321C9-3004-4645-A78D-A85C152062FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1B321C9-3004-4645-A78D-A85C152062FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1B321C9-3004-4645-A78D-A85C152062FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -89,13 +97,14 @@ Global {082C1F69-7932-473F-A700-49584371BE8C} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} {6FE502D4-C43D-49C9-9E57-D1EE566FD1C3} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} {CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} + {7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} {4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} {A8453835-4EE8-4FD4-9766-9C0DCB54CDB3} = {4C48D9D5-1D7F-4616-A05D-256555C310FC} {0C87A7D9-26EA-4821-AF3F-6D28B3006B24} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} {5ECE2E7A-9AE8-49BF-902D-41A7756C3E78} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} {DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} - {7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} {561E0BFB-7817-41FE-BAF2-D78817679AC1} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} + {D1B321C9-3004-4645-A78D-A85C152062FA} = {35C901ED-C234-4A91-9561-AD89B3BB788D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatNotifyController.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatNotifyController.cs new file mode 100644 index 00000000..69848968 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatNotifyController.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SKIT.FlurlHttpClient.Wechat.Security; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Controllers +{ + [ApiController] + [Route("notify")] + public class WechatNotifyController : ControllerBase + { + private readonly ILogger _logger; + + private readonly Options.WechatOptions _wechatOptions; + + public WechatNotifyController( + ILoggerFactory loggerFactory, + IOptions wechatOptions) + { + _logger = loggerFactory.CreateLogger(GetType()); + _wechatOptions = wechatOptions.Value; + } + + [HttpGet] + [Route("message-push")] + public IActionResult VerifyMessage( + [FromQuery(Name = "app_id")] string? appId, + [FromQuery(Name = "signature")] string? signature, + [FromQuery(Name = "timestamp")] string? timestamp, + [FromQuery(Name = "nonce")] string? nonce, + [FromQuery(Name = "echostr")] string? echoString) + { + // 验证服务器推送 + // 文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html + + var wechatAccount = _wechatOptions.Accounts?.FirstOrDefault(e => e.AppId == appId); + if (wechatAccount == null) + return Content("fail"); + + ISet set = new SortedSet() { _wechatOptions.CallbackToken, timestamp!, nonce! }; + string sign = SHA1Utility.Hash(string.Concat(set)); + if (!string.Equals(sign, signature, StringComparison.InvariantCultureIgnoreCase)) + return Content("fail"); + + return Content(echoString); + } + + [HttpPost] + [Route("message-push")] + public async Task ReceiveMessage() + { + // 接收服务器推送 + // 文档:https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html + + using var reader = new StreamReader(Request.Body, Encoding.UTF8); + string content = await reader.ReadToEndAsync(); + _logger.LogInformation("接收到微信推送的数据:{0}", content); + + return Content("success"); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatUserController.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatUserController.cs new file mode 100644 index 00000000..517a5254 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Controllers/WechatUserController.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SKIT.FlurlHttpClient.Wechat.Api; +using SKIT.FlurlHttpClient.Wechat.Api.Models; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Controllers +{ + [ApiController] + [Route("user")] + public class WechatUserController : ControllerBase + { + private readonly ILogger _logger; + + private readonly Services.Repositories.IWechatAccessTokenEntityRepository _wechatAccessTokenEntityRepository; + + private readonly Services.HttpClients.IWechatApiHttpClientFactory _wechatApiHttpClientFactory; + + public WechatUserController( + ILoggerFactory loggerFactory, + Services.Repositories.IWechatAccessTokenEntityRepository wechatAccessTokenEntityRepository, + Services.HttpClients.IWechatApiHttpClientFactory wechatApiHttpClientFactory) + { + _logger = loggerFactory.CreateLogger(GetType()); + _wechatAccessTokenEntityRepository = wechatAccessTokenEntityRepository; + _wechatApiHttpClientFactory = wechatApiHttpClientFactory; + } + + [HttpGet] + [Route("info")] + public async Task GetUserInfoByOpenId( + [FromQuery(Name = "app_id")] string appId, + [FromQuery(Name = "open_id")] string openId) + { + // 获取用户基本信息 + // 文档:https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId + + var entity = _wechatAccessTokenEntityRepository.FirstOrDefault(e => e.AppId == appId); + var client = _wechatApiHttpClientFactory.Create(appId); + var request = new CgibinUserInfoRequest() { AccessToken = entity?.AccessToken, OpenId = openId }; + var response = await client.ExecuteCgibinUserInfoAsync(request, HttpContext.RequestAborted); + if (!response.IsSuccessful()) + { + _logger.LogWarning( + "获取用户基本信息失败(状态码:{0},错误代码:{1},错误描述:{2})。", + response.RawStatus, response.ErrorCode, response.ErrorMessage + ); + } + + return new JsonResult(response); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Models/WechatAccessTokenEntity.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Models/WechatAccessTokenEntity.cs new file mode 100644 index 00000000..01239a1c --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Models/WechatAccessTokenEntity.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Models +{ + public class WechatAccessTokenEntity + { + public string AppId { get; set; } = string.Empty; + + public string AccessToken { get; set; } = string.Empty; + + public long ExpireTimestamp { get; set; } + + public long UpdateTimestamp { get; set; } + + public long CreateTimestamp { get; set; } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Options/WechatOptions.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Options/WechatOptions.cs new file mode 100644 index 00000000..7685cd8e --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Options/WechatOptions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Options +{ + public partial class WechatOptions : IOptions + { + WechatOptions IOptions.Value => this; + + public WechatAccount[] Accounts { get; set; } = Array.Empty(); + + public string CallbackState { get; set; } = string.Empty; + + public string CallbackToken { get; set; } = string.Empty; + } + + partial class WechatOptions + { + public class WechatAccount + { + public string? GhId { get; set; } + + public string AppId { get; set; } = string.Empty; + + public string AppSecret { get; set; } = string.Empty; + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Program.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Program.cs new file mode 100644 index 00000000..949a0055 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5 +{ + public class Program + { + public static void Main(string[] args) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(builder => + { + builder.UseStartup(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Properties/launchSettings.json b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Properties/launchSettings.json new file mode 100644 index 00000000..8c8eb175 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "SKIT.FlurlHttpClient.Wechat.Api.Demo": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.csproj b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.csproj new file mode 100644 index 00000000..8054d2ee --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + enable + true + + + + + + + + + + + + + diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/BackgroundServices/WechatAccessTokenRefreshingBackgroundService.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/BackgroundServices/WechatAccessTokenRefreshingBackgroundService.cs new file mode 100644 index 00000000..abd6018b --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/BackgroundServices/WechatAccessTokenRefreshingBackgroundService.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SKIT.FlurlHttpClient.Wechat.Api; +using SKIT.FlurlHttpClient.Wechat.Api.Models; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.BackgroundServices +{ + class WechatAccessTokenRefreshingBackgroundService : BackgroundService + { + private readonly ILogger _logger; + private readonly Options.WechatOptions _wechatOptions; + private readonly DistributedLock.IDistributedLockFactory _distributedLockFactory; + private readonly HttpClients.IWechatApiHttpClientFactory _wechatApiHttpClientFactory; + private readonly Repositories.IWechatAccessTokenEntityRepository _wechatAccessTokenEntityRepository; + + public WechatAccessTokenRefreshingBackgroundService( + ILoggerFactory loggerFactory, + IOptions wechatOptions, + DistributedLock.IDistributedLockFactory distributedLockFactory, + HttpClients.IWechatApiHttpClientFactory wechatApiHttpClientFactory, + Repositories.IWechatAccessTokenEntityRepository wechatAccessTokenEntityRepository) + { + _logger = loggerFactory.CreateLogger(GetType()); + _wechatOptions = wechatOptions.Value; + _distributedLockFactory = distributedLockFactory; + _wechatApiHttpClientFactory = wechatApiHttpClientFactory; + _wechatAccessTokenEntityRepository = wechatAccessTokenEntityRepository; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + IList tasks = new List(); + foreach (var wechatAccount in _wechatOptions.Accounts) + { + Task task = TryRefreshWechatAccessTokenAsync(wechatAccount.AppId, stoppingToken); + tasks.Add(task); + } + + await Task.WhenAll(tasks); + await Task.Delay(1 * 60 * 1000); // 每隔 1 分钟轮询刷新 + } + } + + private async Task TryRefreshWechatAccessTokenAsync(string appId, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(appId)) + return; // 无效参数 + + var entity = _wechatAccessTokenEntityRepository.FirstOrDefault(e => e.AppId == appId); + if (entity?.ExpireTimestamp > DateTimeOffset.Now.ToUnixTimeSeconds()) + return; // AccessToken 未过期 + + var locker = _distributedLockFactory.Create("accessToken:" + appId); + using var lockHandler = await locker.TryAcquireAsync(TimeSpan.FromSeconds(15), cancellationToken); + if (lockHandler == null) + return; // 未取得锁 + + var client = _wechatApiHttpClientFactory.Create(appId); + var request = new CgibinTokenRequest(); + var response = await client.ExecuteCgibinTokenAsync(request, cancellationToken); + if (!response.IsSuccessful()) + { + _logger.LogWarning( + "刷新 AppId 为 {0} 微信 AccessToken 失败(状态码:{1},错误代码:{2},错误描述:{3})。", + appId, response.RawStatus, response.ErrorCode, response.ErrorMessage + ); + return; // 请求失败 + } + + long nextExpireTimestamp = DateTimeOffset.Now + .AddSeconds(response.ExpiresIn) + .AddMinutes(-10) + .ToUnixTimeSeconds(); // 提前十分钟过期,以便于系统能及时刷新,防止因在过期临界点时出现问题 + if (entity == null) + { + entity = new Models.WechatAccessTokenEntity() + { + AppId = appId, + AccessToken = response.AccessToken, + ExpireTimestamp = nextExpireTimestamp + }; + _wechatAccessTokenEntityRepository.Insert(entity); + } + else + { + entity.AccessToken = response.AccessToken; + entity.ExpireTimestamp = nextExpireTimestamp; + _wechatAccessTokenEntityRepository.Update(entity); + } + + _logger.LogInformation("刷新 AppId 为 {0} 的微信 AccessToken 成功。", appId); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/IDistributedLockFactory.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/IDistributedLockFactory.cs new file mode 100644 index 00000000..437a0f33 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/IDistributedLockFactory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Medallion.Threading; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.DistributedLock +{ + public interface IDistributedLockFactory + { + IDistributedLock Create(string lockName); + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/Implements/DistributedLockFactory.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/Implements/DistributedLockFactory.cs new file mode 100644 index 00000000..7f416b73 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/DistributedLock/Implements/DistributedLockFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Medallion.Threading; +using Medallion.Threading.FileSystem; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.DistributedLock.Implements +{ + class DistributedLockFactory : IDistributedLockFactory + { + private readonly DirectoryInfo _lockFileDirectory = new DirectoryInfo(Environment.CurrentDirectory); + + public IDistributedLock Create(string lockName) + { + return new FileDistributedLock(_lockFileDirectory, lockName); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/IWechatApiHttpClientFactory.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/IWechatApiHttpClientFactory.cs new file mode 100644 index 00000000..ee0cf3e6 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/IWechatApiHttpClientFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.HttpClients +{ + public interface IWechatApiHttpClientFactory + { + WechatApiClient Create(string appId); + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/Implements/WechatApiHttpClientFactory.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/Implements/WechatApiHttpClientFactory.cs new file mode 100644 index 00000000..d5685b01 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/HttpClients/Implements/WechatApiHttpClientFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Flurl; +using Flurl.Http; +using Flurl.Http.Configuration; +using Microsoft.Extensions.Options; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.HttpClients.Implements +{ + partial class WechatApiHttpClientFactory : IWechatApiHttpClientFactory + { + private readonly System.Net.Http.IHttpClientFactory _httpClientFactory; + private readonly Options.WechatOptions _wechatOptions; + + public WechatApiHttpClientFactory( + System.Net.Http.IHttpClientFactory httpClientFactory, + IOptions wechatOptions) + { + _httpClientFactory = httpClientFactory; + _wechatOptions = wechatOptions.Value; + } + + public WechatApiClient Create(string appId) + { + var wechatAccount = _wechatOptions.Accounts?.FirstOrDefault(e => string.Equals(appId, e.AppId)); + if (wechatAccount == null) + throw new Exception("未在配置项中找到该 AppId 对应的微信账号。"); + + FlurlHttp.GlobalSettings.FlurlClientFactory = new DelegatingFlurlClientFactory(_httpClientFactory); + + return new WechatApiClient(new WechatApiClientOptions() + { + AppId = wechatAccount.AppId, + AppSecret = wechatAccount.AppSecret + }); + } + } + + partial class WechatApiHttpClientFactory + { + internal class DelegatingFlurlClientFactory : IFlurlClientFactory + { + private readonly System.Net.Http.IHttpClientFactory _httpClientFactory; + + public DelegatingFlurlClientFactory(System.Net.Http.IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + } + + public IFlurlClient Get(Url url) + { + return new FlurlClient(_httpClientFactory.CreateClient(url.ToUri().Host)); + } + + public void Dispose() + { + // Do Nothing + } + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/IWechatAccessTokenEntityRepository.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/IWechatAccessTokenEntityRepository.cs new file mode 100644 index 00000000..58446af4 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/IWechatAccessTokenEntityRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.Repositories +{ + public interface IWechatAccessTokenEntityRepository : IEnumerable + { + void Insert(Models.WechatAccessTokenEntity entity); + + void Update(Models.WechatAccessTokenEntity entity); + + void Delete(Models.WechatAccessTokenEntity entity); + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/GlobalDatabase.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/GlobalDatabase.cs new file mode 100644 index 00000000..e167288c --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/GlobalDatabase.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NMemory; +using NMemory.Tables; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.Repositories.Implements +{ + internal class GlobalDatabase + { + static GlobalDatabase() + { + Database db = new Database(); + + TableWechatAccessTokenEntity = db.Tables.Create(e => e.AppId); + } + + public static Table TableWechatAccessTokenEntity { get; } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/WechatAccessTokenEntityRepository.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/WechatAccessTokenEntityRepository.cs new file mode 100644 index 00000000..7fe4fbb1 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Services/Repositories/Implements/WechatAccessTokenEntityRepository.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5.Services.Repositories.Implements +{ + public class WechatAccessTokenEntityRepository : IWechatAccessTokenEntityRepository + { + public void Insert(Models.WechatAccessTokenEntity entity) + { + entity.CreateTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + entity.UpdateTimestamp = entity.CreateTimestamp; + GlobalDatabase.TableWechatAccessTokenEntity.Insert(entity); + } + + public void Update(Models.WechatAccessTokenEntity entity) + { + entity.UpdateTimestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); + GlobalDatabase.TableWechatAccessTokenEntity.Update(entity); + } + + public void Delete(Models.WechatAccessTokenEntity entity) + { + GlobalDatabase.TableWechatAccessTokenEntity.Delete(entity); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GlobalDatabase.TableWechatAccessTokenEntity.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GlobalDatabase.TableWechatAccessTokenEntity.GetEnumerator(); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Startup.cs b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Startup.cs new file mode 100644 index 00000000..8e1734d2 --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/Startup.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5 +{ + public class Startup + { + public IConfiguration Configuration { get; } + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + // ע + services.AddOptions(); + services.Configure(Configuration.GetSection(nameof(Options.WechatOptions))); + + // עֲʽ + services.AddSingleton(); + + // עִ + services.AddSingleton(); + + // ע빤 HTTP ͻ + services.AddHttpClient(); + services.AddSingleton(); + + // ע̨ + services.AddHostedService(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/appsettings.json b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/appsettings.json new file mode 100644 index 00000000..fc35277b --- /dev/null +++ b/samples/SKIT.FlurlHttpClient.Wechat.Api.Sample_Net5/appsettings.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "AllowedHosts": "*", + + "WechatOptions": { + "Accounts": [ + { + "AppId": "填写 AppId", + "AppSecret": "填写 AppSecret" + } + ], + "CallbackState": "SKIT.FlurlHttpClient.Wechat", + "CallbackToken": "填写服务器推送的 Token" + } +}