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 1e959bca..4208d949 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 @@ -11,6 +11,7 @@ using Volo.Abp.Caching; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao; using Yi.Framework.AiHub.Domain.Managers.Fuwuhao; +using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao; using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; using Yi.Framework.Rbac.Application.Contracts.IServices; @@ -26,12 +27,15 @@ public class FuwuhaoService : ApplicationService private readonly IHttpContextAccessor _accessor; private readonly FuwuhaoManager _fuwuhaoManager; private IDistributedCache _sceneCache; + private IDistributedCache _openIdToSceneCache; + 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) + IDistributedCache sceneCache, IAccountService accountService, IFileService fileService, + IDistributedCache openIdToSceneCache) { _logger = logger; _accessor = accessor; @@ -39,6 +43,7 @@ public class FuwuhaoService : ApplicationService _sceneCache = sceneCache; _accountService = accountService; _fileService = fileService; + _openIdToSceneCache = openIdToSceneCache; } /// @@ -63,7 +68,7 @@ public class FuwuhaoService : ApplicationService public async Task PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce) { - _fuwuhaoManager.ValidateCallback(signature,timestamp,nonce); + _fuwuhaoManager.ValidateCallback(signature, timestamp, nonce); var request = _accessor.HttpContext.Request; // 1. 读取原始 XML 内容 using var reader = new StreamReader(request.Body, Encoding.UTF8); @@ -74,11 +79,12 @@ public class FuwuhaoService : ApplicationService var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader); //获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可 - var scene = body.EventKey.Replace("qrscene_",""); + var scene = body.EventKey.Replace("qrscene_", ""); if (!(body.Event is "SCAN" or "subscribe")) { throw new UserFriendlyException("当前回调只处理扫码 与 关注"); } + if (scene is null) { throw new UserFriendlyException("服务号返回无场景值"); @@ -86,22 +92,35 @@ public class FuwuhaoService : ApplicationService //制作幂等 await using (var handle = - await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}", TimeSpan.FromSeconds(60))) + await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}", + TimeSpan.FromSeconds(60))) { if (handle == null) { return "success"; // 跳过直接返回成功 } - var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}"); + var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}"); //根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成 var scenResult = await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId); cache.SceneResult = scenResult; - await _sceneCache.SetAsync($"fuwuhao:{scene}", cache, + await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", cache, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) }); + + + //如果是注册,将OpenId与Scene进行绑定,代表用户有30分钟进行注册 + if (scenResult == SceneResultEnum.Register) + { + await _openIdToSceneCache.SetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{body.FromUserName}", scene, + new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); + + var replyMessage = + _fuwuhaoManager.BuildRegisterMessage(body.FromUserName); + return replyMessage; + } } return "success"; @@ -118,7 +137,7 @@ public class FuwuhaoService : ApplicationService //生成一个随机场景值 var scene = Guid.NewGuid().ToString("N"); var qrCodeUrl = await _fuwuhaoManager.CreateQrCodeAsync(scene); - await _sceneCache.SetAsync($"fuwuhao:{scene}", new SceneCacheDto() + await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto() { UserId = CurrentUser.IsAuthenticated ? CurrentUser.GetId() : null, SceneType = sceneType @@ -139,7 +158,7 @@ public class FuwuhaoService : ApplicationService public async Task GetQrCodeResultAsync([FromQuery] string scene) { var output = new QrCodeResultOutput(); - var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}"); + var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}"); if (cache is null) { output.SceneResult = SceneResultEnum.Expired; @@ -164,13 +183,23 @@ public class FuwuhaoService : ApplicationService /// /// /// - [HttpPost("fuwuhao/register")] - public async Task RegisterByCodeAsync([FromQuery] string code) + [HttpGet("fuwuhao/register")] + public async Task RegisterByCodeAsync([FromQuery] string code) { //根据code获取到openid、微信用户昵称、头像 var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code); - var files = new FormFileCollection(); + if (userInfo is null) + { + return "当前注册已经失效,请重新扫码注册"; + } + var scene = await _openIdToSceneCache.GetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{userInfo.OpenId}"); + if (scene is null) + { + return "当前注册已经过期,请重新扫码注册"; + } + + var files = new FormFileCollection(); // 下载头像并添加到系统文件中 if (!string.IsNullOrEmpty(userInfo.HeadImgUrl)) { @@ -190,16 +219,22 @@ public class FuwuhaoService : ApplicationService var result = await _fileService.Post(files); - var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto + await _accountService.PostSystemRegisterAsync(new RegisterDto { - UserName = userInfo.Nickname, + UserName = $"wx{Random.Shared.Next(100000, 999999)}", Password = Guid.NewGuid().ToString("N"), Phone = null, Email = null, Nick = userInfo.Nickname, Icon = result.FirstOrDefault()?.Id.ToString() }); - return await _accountService.PostLoginAsync(userId); + + var sceneCache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}"); + sceneCache.SceneResult = SceneResultEnum.Login; + await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}:{scene}", sceneCache, + new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) }); + + return "恭喜你已注册成功意社区账号!"; } } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/FuwuhaoConst.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/FuwuhaoConst.cs new file mode 100644 index 00000000..5511e4ce --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Consts/FuwuhaoConst.cs @@ -0,0 +1,7 @@ +namespace Yi.Framework.AiHub.Domain.Shared.Consts; + +public class FuwuhaoConst +{ + public const string SceneCacheKey = "fuwuhao:scene:"; + public const string OpenIdToSceneCacheKey = "fuwuhao:OpenIdToScene:"; +} \ No newline at end of file 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 665acdf5..84dfb571 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 @@ -19,7 +19,8 @@ public class FuwuhaoManager : DomainService private ISqlSugarRepository _userRepository; public FuwuhaoManager(IOptions options, IHttpClientFactory httpClientFactory, - ISqlSugarRepository userRepository, IDistributedCache accessTokenCache) + ISqlSugarRepository userRepository, + IDistributedCache accessTokenCache) { _options = options.Value; _httpClientFactory = httpClientFactory; @@ -97,7 +98,8 @@ public class FuwuhaoManager : DomainService /// 用户基础信息响应对象 private async Task GetBaseUserInfoByCodeAsync(string code) { - var url = $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={_options.AppId}&secret={_options.Secret}&code={code}&grant_type=authorization_code"; + var url = + $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={_options.AppId}&secret={_options.Secret}&code={code}&grant_type=authorization_code"; var response = await _httpClientFactory.CreateClient().GetAsync(url); response.EnsureSuccessStatusCode(); @@ -116,10 +118,11 @@ public class FuwuhaoManager : DomainService /// /// 授权码 /// 用户信息响应对象 - public async Task GetUserInfoByCodeAsync(string code) + public async Task GetUserInfoByCodeAsync(string code) { - var baseUserInfo = await GetBaseUserInfoByCodeAsync(code); - var url = $"https://api.weixin.qq.com/sns/userinfo?access_token={baseUserInfo.AccessToken}&openid={baseUserInfo.OpenId}&lang=zh_CN"; + var baseUserInfo = await GetBaseUserInfoByCodeAsync(code); + var url = + $"https://api.weixin.qq.com/sns/userinfo?access_token={baseUserInfo.AccessToken}&openid={baseUserInfo.OpenId}&lang=zh_CN"; var response = await _httpClientFactory.CreateClient().GetAsync(url); response.EnsureSuccessStatusCode(); @@ -142,11 +145,11 @@ public class FuwuhaoManager : DomainService 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); @@ -164,8 +167,40 @@ public class FuwuhaoManager : DomainService } } } - - + + + /// + /// 构建引导注册图文消息体 + /// + /// 接收用户的OpenID + /// 图文消息标题 + /// 图文消息描述 + /// XML格式的图文消息体 + public string BuildRegisterMessage(string toUser, string title="意社区点击一键注册账号", string description="来自意社区SSO统一注册安全中心") + { + var createTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var fromUser = _options.FromUser; + var url = + $"https://open.weixin.qq.com/connect/oauth2/authorize?appid={_options.AppId}&redirect_uri={_options.RedirectUri}&response_type=code&scope=snsapi_userinfo&state={createTime}#wechat_redirect"; + var xml = $@" + + + {createTime} + + 1 + + + <![CDATA[{title}]]> + + + + + +"; + return xml; + } + + /// /// 处理回调逻辑 /// 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 28e57cac..281202b0 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 @@ -6,7 +6,7 @@ public class FuwuhaoOptions /// 微信公众号AppId /// public string AppId { get; set; } - + /// /// 微信公众号AppSecret /// @@ -16,4 +16,19 @@ public class FuwuhaoOptions /// 回调token /// public string CallbackToken { get; set; } + + /// + /// 微信公众号原始ID(用于FromUser) + /// + public string FromUser { get; set; } + + /// + /// 微信网页授权跳转地址 + /// + public string RedirectUri { get; set; } + + /// + /// 图片地址 + /// + public string PicUrl { get; set; } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs index 0a2359ae..d8475991 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Text; using Mapster; +using Medallion.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -36,6 +37,13 @@ namespace Yi.Framework.Rbac.Domain.Managers private UserManager _userManager; private ISqlSugarRepository _roleRepository; private RefreshJwtOptions _refreshJwtOptions; + /// + /// 缓存前缀 + /// + private string CacheKeyPrefix => LazyServiceProvider.LazyGetRequiredService>() + .Value.KeyPrefix; + public IDistributedLockProvider DistributedLock => + LazyServiceProvider.LazyGetService(); public AccountManager(IUserRepository repository , IOptions jwtOptions @@ -288,17 +296,32 @@ namespace Yi.Framework.Rbac.Domain.Managers /// /// public async Task RegisterAsync(string userName, string password, long? phone, string? email, - string? nick,string? icon) + string? nick, string? icon) { - if (phone is null && string.IsNullOrWhiteSpace(email)) + if (userName is null) { - throw new UserFriendlyException("注册时,电话与邮箱不能同时为空"); + throw new UserFriendlyException("注册时,用户名不能为空"); } - var user = new UserAggregateRoot(userName, password, phone, email, nick); - var userId = await _userManager.CreateAsync(user); - await _userManager.SetDefautRoleAsync(user.Id); - return userId; + //制作幂等 + await using (var handle = await DistributedLock.TryAcquireLockAsync($"{CacheKeyPrefix}Register:Lock:{userName}", TimeSpan.FromSeconds(60))) + { + if (handle is null) + { + throw new UserFriendlyException($"{userName}用户正在注册中,请稍等"); + } + + var userUpName = userName.ToUpper(); + if (await _userManager._repository._DbQueryable.Where(x => x.UserName.ToUpper() == userUpName).AnyAsync()) + { + throw new UserFriendlyException($"{userName}用户已注册"); + } + + var user = new UserAggregateRoot(userName, password, phone, email, nick); + var userId = await _userManager.CreateAsync(user); + await _userManager.SetDefautRoleAsync(user.Id); + return userId; + } } } } \ No newline at end of file