feat: 增加服务号回调签名校验及扫码回调幂等处理

- `FuwuhaoManager` 新增 `ValidateCallback` 方法,用于校验微信回调签名
- `FuwuhaoOptions` 增加 `CallbackToken` 配置项
- `QrCodeResponse` 属性添加 `JsonPropertyName` 标注,支持 JSON 序列化映射
- `FuwuhaoService` 在回调接口中增加签名校验,并通过分布式锁实现幂等处理
- 调整场景值解析逻辑,过滤非扫码/关注事件
- 优化缓存过期时间设置
This commit is contained in:
chenchun
2025-08-28 15:20:15 +08:00
parent b768bca638
commit 1d108983e8
5 changed files with 78 additions and 17 deletions

View File

@@ -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<SceneCacheDto> _sceneCache;
private IAccountService _accountService;
private IFileService _fileService;
public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService)
@@ -56,12 +58,12 @@ public class FuwuhaoService : ApplicationService
/// <param name="signature"></param>
/// <param name="timestamp"></param>
/// <param name="nonce"></param>
/// <param name="openId"></param>
/// <returns></returns>
[HttpPost("fuwuhao/callback")]
public async Task<string> 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