From 1d108983e83d0d2e3aa1c6a4d101841c454d547e Mon Sep 17 00:00:00 2001 From: chenchun Date: Thu, 28 Aug 2025 15:20:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=8F=B7=E5=9B=9E=E8=B0=83=E7=AD=BE=E5=90=8D=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=8F=8A=E6=89=AB=E7=A0=81=E5=9B=9E=E8=B0=83=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `FuwuhaoManager` 新增 `ValidateCallback` 方法,用于校验微信回调签名 - `FuwuhaoOptions` 增加 `CallbackToken` 配置项 - `QrCodeResponse` 属性添加 `JsonPropertyName` 标注,支持 JSON 序列化映射 - `FuwuhaoService` 在回调接口中增加签名校验,并通过分布式锁实现幂等处理 - 调整场景值解析逻辑,过滤非扫码/关注事件 - 优化缓存过期时间设置 --- .../Dtos/Fuwuhao/SceneCacheDto.cs | 2 - .../Services/FuwuhaoService.cs | 41 ++++++++++++------ .../Managers/Fuwuhao/FuwuhaoManager.cs | 42 +++++++++++++++++-- .../Managers/Fuwuhao/FuwuhaoOptions.cs | 5 +++ .../Managers/Fuwuhao/QrCodeResponse.cs | 5 +++ 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Fuwuhao/SceneCacheDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Fuwuhao/SceneCacheDto.cs index ac9615d2..1a17b8ef 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Fuwuhao/SceneCacheDto.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Fuwuhao/SceneCacheDto.cs @@ -4,8 +4,6 @@ namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao; public class SceneCacheDto { - public string Scene { get; set; } - public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait; public SceneTypeEnum SceneType { get; set; } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FuwuhaoService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FuwuhaoService.cs index 440aceb7..1e959bca 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FuwuhaoService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/FuwuhaoService.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Json; using System.Xml.Serialization; +using Medallion.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; @@ -27,6 +28,7 @@ public class FuwuhaoService : ApplicationService private IDistributedCache _sceneCache; private IAccountService _accountService; private IFileService _fileService; + public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService(); public FuwuhaoService(ILogger logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager, IDistributedCache sceneCache, IAccountService accountService, IFileService fileService) @@ -56,12 +58,12 @@ public class FuwuhaoService : ApplicationService /// /// /// - /// /// [HttpPost("fuwuhao/callback")] public async Task PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce) { + _fuwuhaoManager.ValidateCallback(signature,timestamp,nonce); var request = _accessor.HttpContext.Request; // 1. 读取原始 XML 内容 using var reader = new StreamReader(request.Body, Encoding.UTF8); @@ -72,21 +74,35 @@ public class FuwuhaoService : ApplicationService var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader); //获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可 - var scene = body.EventKey; - + var scene = body.EventKey.Replace("qrscene_",""); + if (!(body.Event is "SCAN" or "subscribe")) + { + throw new UserFriendlyException("当前回调只处理扫码 与 关注"); + } if (scene is null) { throw new UserFriendlyException("服务号返回无场景值"); } - var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}"); + //制作幂等 + await using (var handle = + await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}", TimeSpan.FromSeconds(60))) + { + if (handle == null) + { + return "success"; // 跳过直接返回成功 + } - //根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成 - var scenResult = await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId); - cache.SceneResult = scenResult; + var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}"); - await _sceneCache.SetAsync($"fuwuhao:{scene}", cache, - new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10) }); + //根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成 + var scenResult = + await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId); + cache.SceneResult = scenResult; + + await _sceneCache.SetAsync($"fuwuhao:{scene}", cache, + new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) }); + } return "success"; } @@ -154,16 +170,16 @@ public class FuwuhaoService : ApplicationService //根据code获取到openid、微信用户昵称、头像 var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code); var files = new FormFileCollection(); - + // 下载头像并添加到系统文件中 if (!string.IsNullOrEmpty(userInfo.HeadImgUrl)) { using var httpClient = new HttpClient(); var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl); var imageStream = new MemoryStream(imageBytes); - + // 从URL中提取文件扩展名,默认为png - var fileName = $"avatar_{userInfo.OpenId}.png"; + var fileName = $"avatar_{userInfo.OpenId}.png"; var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName) { Headers = new HeaderDictionary(), @@ -171,6 +187,7 @@ public class FuwuhaoService : ApplicationService }; files.Add(formFile); } + var result = await _fileService.Post(files); var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoManager.cs index 1a3c9d5d..665acdf5 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoManager.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Options; @@ -18,11 +19,12 @@ public class FuwuhaoManager : DomainService private ISqlSugarRepository _userRepository; public FuwuhaoManager(IOptions options, IHttpClientFactory httpClientFactory, - ISqlSugarRepository userRepository) + ISqlSugarRepository userRepository, IDistributedCache accessTokenCache) { _options = options.Value; _httpClientFactory = httpClientFactory; _userRepository = userRepository; + _accessTokenCache = accessTokenCache; } /// @@ -66,11 +68,11 @@ public class FuwuhaoManager : DomainService var requestBody = new { action_name = "QR_STR_SCENE", + expire_seconds = 600, action_info = new { scene = new { - expire_seconds = 600, scene_str = scene } } @@ -130,6 +132,40 @@ public class FuwuhaoManager : DomainService return result; } + /// + /// 校验微信服务器回调参数是否正确 + /// + /// 微信加密签名 + /// 时间戳 + /// 随机数 + /// true表示验证通过,false表示验证失败 + public void ValidateCallback(string signature, string timestamp, string nonce) + { + var token = _options.CallbackToken; // 您设置的token + + // 将token、timestamp、nonce三个参数进行字典序排序 + var parameters = new[] { token, timestamp, nonce }; + Array.Sort(parameters); + + // 将三个参数字符串拼接成一个字符串 + var concatenated = string.Join("", parameters); + + // 进行SHA1计算签名 + using (var sha1 = SHA1.Create()) + { + var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(concatenated)); + var calculatedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower(); + + // 与URL链接中的signature参数进行对比 + var result = calculatedSignature.Equals(signature, StringComparison.OrdinalIgnoreCase); + if (!result) + { + throw new UserFriendlyException("服务号回调签名异常"); + } + } + } + + /// /// 处理回调逻辑 /// diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoOptions.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoOptions.cs index 46f53274..28e57cac 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoOptions.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/FuwuhaoOptions.cs @@ -11,4 +11,9 @@ public class FuwuhaoOptions /// 微信公众号AppSecret /// public string Secret { get; set; } + + /// + /// 回调token + /// + public string CallbackToken { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/QrCodeResponse.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/QrCodeResponse.cs index e641539e..da2b7d5f 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/QrCodeResponse.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/Fuwuhao/QrCodeResponse.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao; /// @@ -8,15 +10,18 @@ public class QrCodeResponse /// /// 二维码票据 /// + [JsonPropertyName("ticket")] public string Ticket { get; set; } /// /// 过期时间(秒) /// + [JsonPropertyName("expire_seconds")] public int ExpireSeconds { get; set; } /// /// 二维码URL /// + [JsonPropertyName("url")] public string Url { get; set; } } \ No newline at end of file