diff --git a/YD_Event.Application/Client/Dtos/MiniProgramDtos.cs b/YD_Event.Application/Client/Dtos/MiniProgramDtos.cs new file mode 100644 index 0000000..87e9525 --- /dev/null +++ b/YD_Event.Application/Client/Dtos/MiniProgramDtos.cs @@ -0,0 +1,133 @@ +using System.ComponentModel.DataAnnotations; + +namespace YD_Event.Application.Client.Dtos; + +public class WeChatLoginInput +{ + /// + /// 微信临时授权码 + /// + [Required(ErrorMessage = "code不能为空")] + public string Code { get; set; } + + /// + /// 加密数据 + /// + [Required(ErrorMessage = "encryptedData不能为空")] + public string EncryptedData { get; set; } + + /// + /// 初始向量 + /// + [Required(ErrorMessage = "iv不能为空")] + public string Iv { get; set; } + + /// + /// 原始数据 + /// + [Required(ErrorMessage = "rawData不能为空")] + public string RawData { get; set; } + + /// + /// 签名 + /// + [Required(ErrorMessage = "signature不能为空")] + public string Signature { get; set; } +} + +public class WeChatLoginOutput +{ + public string Openid { get; set; } + public string SessionKey { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public int ExpiresIn { get; set; } + public object UserInfo { get; set; } // 简化的用户信息对象 +} + +public class WeChatRegisterInput +{ + /// + /// OpenId + /// + [Required(ErrorMessage = "openid不能为空")] + public string Openid { get; set; } + + /// + /// 加密数据 + /// + [Required(ErrorMessage = "encryptedData不能为空")] + public string EncryptedData { get; set; } + + /// + /// 初始向量 + /// + [Required(ErrorMessage = "iv不能为空")] + public string Iv { get; set; } + + /// + /// 原始数据 + /// + [Required(ErrorMessage = "rawData不能为空")] + public string RawData { get; set; } + + /// + /// 签名 + /// + [Required(ErrorMessage = "signature不能为空")] + public string Signature { get; set; } + + /// + /// 邀请码 (可选) + /// + public string? InviteCode { get; set; } +} + +public class WeChatRegisterOutput +{ + public long UserId { get; set; } + public string Nickname { get; set; } + public string AvatarUrl { get; set; } + public DateTime RegisterTime { get; set; } +} + +public class WeChatResetPasswordInput +{ + [Required(ErrorMessage = "openid不能为空")] + public string Openid { get; set; } + + [Required(ErrorMessage = "encryptedData不能为空")] + public string EncryptedData { get; set; } + + [Required(ErrorMessage = "iv不能为空")] + public string Iv { get; set; } + + public string? Email { get; set; } + public string? SmsCode { get; set; } +} + +public class WeChatResetPasswordOutput +{ + public string ResetToken { get; set; } + public DateTime ExpireTime { get; set; } + public string SendChannel { get; set; } // "email" or "sms" +} + +public class WeChatUserInfo +{ + public string OpenId { get; set; } + public string NickName { get; set; } + public int Gender { get; set; } + public string City { get; set; } + public string Province { get; set; } + public string Country { get; set; } + public string AvatarUrl { get; set; } + public string UnionId { get; set; } + public Watermark Watermark { get; set; } +} + +public class Watermark +{ + public long Timestamp { get; set; } + public string Appid { get; set; } +} diff --git a/YD_Event.Application/Client/Dtos/OAuthAccountDetailOutput.cs b/YD_Event.Application/Client/Dtos/OAuthAccountDetailOutput.cs deleted file mode 100644 index bf4164f..0000000 --- a/YD_Event.Application/Client/Dtos/OAuthAccountDetailOutput.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace YD_Event.Application.Client.Dtos; - -public class OAuthAccountDetailOutput -{ - /// - /// 用户ID - /// - public long Id { get; set; } - - /// - /// 昵称 - /// - public string NickName { get; set; } - /// - /// 头像 - /// - public string Avatar { get; set; } -} \ No newline at end of file diff --git a/YD_Event.Application/Client/OAuthController.cs b/YD_Event.Application/Client/OAuthController.cs index 62ce368..3ce6643 100644 --- a/YD_Event.Application/Client/OAuthController.cs +++ b/YD_Event.Application/Client/OAuthController.cs @@ -1,185 +1,319 @@ -/* -using YD_Event.Application.Auth; +using Microsoft.Extensions.Logging; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using YD_Event.Application.Client.Dtos; -using MrHuo.OAuth.QQ; +using YD_Event.Core.Utils; +using YD_Event.Application.Auth; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json; namespace YD_Event.Application.Client; + /// -/// 第三方授权登录 +/// 微信小程序认证接口 /// [ApiDescriptionSettings("微信小程序接口")] +[Route("api/v1/mini-program/auth")] public class OAuthController : IDynamicApiController { - /// - /// 第三方授权缓存 - /// - private const string OAuthKey = "oauth."; - /// - /// 授权成功后回调页面缓存键 - /// - private const string OAuthRedirectKey = "oauth.redirect."; - - private readonly QQOAuth _qqoAuth; - private readonly AuthManager _authManager; + private readonly WeChatTool _weChatTool; private readonly ISqlSugarRepository _accountRepository; + private readonly ISqlSugarRepository _appUserRepository; + private readonly ISqlSugarRepository _roleRepository; + private readonly ISqlSugarRepository _userRoleRepository; private readonly IEasyCachingProvider _easyCachingProvider; - private readonly IHttpContextAccessor _httpContextAccessor; private readonly IIdGenerator _idGenerator; + private readonly ILogger _logger; - public OAuthController(QQOAuth qqoAuth, - AuthManager authManager, + private const string SessionKeyCachePrefix = "wechat_session_"; + + public OAuthController( + WeChatTool weChatTool, ISqlSugarRepository accountRepository, + ISqlSugarRepository appUserRepository, + ISqlSugarRepository roleRepository, + ISqlSugarRepository userRoleRepository, IEasyCachingProvider easyCachingProvider, - IHttpContextAccessor httpContextAccessor, - IIdGenerator idGenerator) + IIdGenerator idGenerator, + ILogger logger) { - _qqoAuth = qqoAuth; - _authManager = authManager; + _weChatTool = weChatTool; _accountRepository = accountRepository; + _appUserRepository = appUserRepository; + _roleRepository = roleRepository; + _userRoleRepository = userRoleRepository; _easyCachingProvider = easyCachingProvider; - _httpContextAccessor = httpContextAccessor; _idGenerator = idGenerator; + _logger = logger; } /// - /// 获取授权地址 + /// 登录接口 /// - /// 授权登录类型 - /// 回跳地址 + /// /// - [HttpGet("{type}")] + [HttpPost("login")] [AllowAnonymous] - public async Task Get(string type, [FromQuery][Required(ErrorMessage = "缺少参数")] string referer) + public async Task Login([FromBody] WeChatLoginInput input) { - string code = _idGenerator.Encode(_idGenerator.NewLong()); - var request = _httpContextAccessor.HttpContext!.Request; - await _easyCachingProvider.SetAsync($"{OAuthRedirectKey}{code}", referer, TimeSpan.FromMinutes(5)); - string authUrl = type.ToLower() switch + // 1. code2Session + var sessionResult = await _weChatTool.Code2Session(input.Code); + string openid = sessionResult.OpenId; + string sessionKey = sessionResult.SessionKey; + + // 缓存 session_key,用于后续解密 + await _easyCachingProvider.SetAsync($"{SessionKeyCachePrefix}{openid}", sessionKey, TimeSpan.FromHours(2)); + + // 2. 校验签名 (可选,根据业务严谨性) + // var signature2 = Hash(input.RawData + sessionKey); + // if (signature2 != input.Signature) throw Oops.Bah("签名校验失败"); + + // 3. 查找用户 + var account = await _accountRepository.GetFirstAsync(x => x.OAuthId == openid && x.Type == "WeChat"); + + string accessToken = null; + string refreshToken = null; + object userInfo = null; + int expiresIn = 7200; + + if (account != null) { - "qq" => _qqoAuth.GetAuthorizeUrl(code), - _ => throw Oops.Bah("无效请求") + // 生成 Token + (accessToken, refreshToken) = GenerateTokens(account); + + // 获取用户信息 + var appUser = await _appUserRepository.GetFirstAsync(x => x.UserId == account.Id); + if (appUser != null) + { + userInfo = new { appUser.Name, appUser.Id, appUser.Mobile, appUser.Avatar, appUser.NickName }; + } + else + { + userInfo = new { account.Name, account.Avatar }; + } + } + + return new WeChatLoginOutput + { + Openid = openid, + SessionKey = sessionKey, // 需求要求返回 + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpiresIn = expiresIn, + UserInfo = userInfo }; - return authUrl; } /// - /// 授权回调 + /// 注册接口 /// - /// 授权类型 - /// - /// 缓存唯一ID + /// /// - [HttpGet("{type}/callback")] + [HttpPost("register")] [AllowAnonymous] - public async Task Callback(string type, [FromQuery] string code, [FromQuery] string state) + [UnitOfWork] + public async Task Register([FromBody] WeChatRegisterInput input) { - if (string.IsNullOrWhiteSpace(state) || !await _easyCachingProvider.ExistsAsync($"{OAuthRedirectKey}{state}")) - { - throw Oops.Oh("缺少参数"); - } - AuthAccount account; - switch (type.ToLower()) - { - case "qq": - var auth = await _qqoAuth.AuthorizeCallback(code, state); - if (!auth.IsSccess) - { - throw Oops.Bah(auth.ErrorMessage); - } - var info = auth.UserInfo; - string openId = await _qqoAuth.GetOpenId(auth.AccessToken.AccessToken); - account = await _accountRepository.AsQueryable().FirstAsync(x => x.OAuthId == openId && SqlFunc.ToLower(x.Type) == "qq"); - var gender = info.Gender == "男" ? Gender.Male : - info.Gender == "女" ? Gender.Female : Gender.Unknown; - if (account != null) - { - await _accountRepository.UpdateAsync(x => new AuthAccount() - { - Avatar = string.IsNullOrWhiteSpace(info.QQ100Avatar) ? info.Avatar : info.QQ100Avatar, - Name = info.Name, - Gender = gender - }, - x => x.OAuthId == openId && SqlFunc.ToLower(x.Type) == "qq"); - } - else - { - account = await _accountRepository.InsertReturnEntityAsync(new AuthAccount() - { - Gender = gender, - Avatar = info.Avatar, - Name = info.Name, - OAuthId = openId, - Type = "QQ" - }); - } - break; + // 1. 获取 session_key + var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}"; + var sessionKeyCache = await _easyCachingProvider.GetAsync(cacheKey); + if (!sessionKeyCache.HasValue) throw Oops.Bah("SessionKey已过期,请重新登录"); + string sessionKey = sessionKeyCache.Value; - default: - throw Oops.Bah("无效请求"); + // 2. 解密用户信息 + string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv); + var wxUser = JsonConvert.DeserializeObject(json); + if (wxUser == null) throw Oops.Bah("解密用户信息失败"); + + // 验证 watermark + var configAppId = App.Configuration["WeChat:AppId"]; + if (!string.IsNullOrEmpty(configAppId) && wxUser.Watermark?.Appid != configAppId) + { + // 仅记录警告,防止配置不一致导致注册失败 + _logger.LogWarning($"Watermark AppId mismatch: {wxUser.Watermark?.Appid} vs {configAppId}"); } - string key = $"{OAuthKey}{state}"; - await _easyCachingProvider.SetAsync(key, account, TimeSpan.FromMinutes(3)); - //登录成功后的回调页面 - string url = (await _easyCachingProvider.GetAsync($"{OAuthRedirectKey}{state}")).Value;//App.Configuration["oauth:redirect_uri"]?.TrimEnd('/'); - string redirect = url.Contains("?") ? $"{url}&code={state}" : $"{url}?code={state}"; - return new RedirectResult(redirect); + // 3. 写入 AuthAccount + // 检查是否已存在 + if (await _accountRepository.IsAnyAsync(x => x.OAuthId == input.Openid && x.Type == "WeChat")) + { + throw Oops.Bah("该用户已注册"); + } + + long accountId = _idGenerator.NewLong(); + var account = new AuthAccount + { + Id = accountId, + OAuthId = input.Openid, + Type = "WeChat", + Name = wxUser.NickName, + Avatar = wxUser.AvatarUrl, + Gender = (Gender)wxUser.Gender, // 假设 Gender 枚举兼容 + CreatedTime = DateTime.Now, + CreatedUserId = accountId // 自指 + }; + await _accountRepository.InsertAsync(account); + + // 4. 写入 AppUser (Profile) + long appUserId = _idGenerator.NewLong(); + var appUser = new YD_Event.Core.Entities.AppUser + { + Id = appUserId, + UserId = accountId, + Name = wxUser.NickName, + NickName = wxUser.NickName, + Avatar = wxUser.AvatarUrl, + Gender = (Gender)wxUser.Gender, + PersonType = 0, // 普通用户 + Status = AvailabilityStatus.Enable, + AuditStatus = AuditStatus.Pass, + CreatedTime = DateTime.Now, + CreatedUserId = accountId + }; + await _appUserRepository.InsertAsync(appUser); + + // 5. 初始化默认角色 + // 查找默认角色,假设为 "OrdinaryUser" 或 "User" + var defaultRole = await _roleRepository.GetFirstAsync(x => x.Code == "User" || x.Code == "OrdinaryUser"); + if (defaultRole == null) + { + // 如果不存在,创建一个默认角色 + defaultRole = new SysRole + { + Id = _idGenerator.NewLong(), + Name = "普通用户", + Code = "User", + Status = AvailabilityStatus.Enable, + CreatedTime = DateTime.Now + }; + await _roleRepository.InsertAsync(defaultRole); + } + + // 绑定角色 (这里是 AppUser 还是 AuthAccount 绑定角色? SysUserRole 关联 UserId,通常指 AuthAccount.Id 或 SysUser.Id) + // 假设 SysUserRole.UserId 对应 AuthAccount.Id (因为 AuthAccount 是登录主体) + await _userRoleRepository.InsertAsync(new SysUserRole + { + Id = _idGenerator.NewLong(), + UserId = accountId, + RoleId = defaultRole.Id, + CreatedTime = DateTime.Now + }); + + return new WeChatRegisterOutput + { + UserId = accountId, + Nickname = account.Name, + AvatarUrl = account.Avatar, + RegisterTime = account.CreatedTime + }; } /// - /// 登录 + /// 找回密码接口 /// - /// + /// /// - [HttpPost("login/{code}")] + [HttpPost("reset-password")] [AllowAnonymous] - public async Task Login(string code) + public async Task ResetPassword([FromBody] WeChatResetPasswordInput input) { - string key = $"{OAuthKey}{code}", key2 = $"{OAuthRedirectKey}{code}"; - var value = await _easyCachingProvider.GetAsync(key); - if (!value.HasValue) + // 1. 获取 session_key + var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}"; + var sessionKeyCache = await _easyCachingProvider.GetAsync(cacheKey); + if (!sessionKeyCache.HasValue) throw Oops.Bah("SessionKey已过期,请重新登录"); + string sessionKey = sessionKeyCache.Value; + + // 2. 验证微信身份 (尝试解密) + try { - throw Oops.Bah("无效参数"); + string json = DecryptWeChatData(input.EncryptedData, sessionKey, input.Iv); + var wxUser = JsonConvert.DeserializeObject(json); + if (wxUser.OpenId != input.Openid) throw Oops.Bah("身份验证失败: OpenId不匹配"); } + catch (Exception ex) + { + _logger.LogError(ex, "ResetPassword Decrypt Failed"); + throw Oops.Bah("身份验证失败"); + } + + // 3. 生成一次性 Token + string resetToken = Guid.NewGuid().ToString("N"); + + // 4. 发送邮件/短信 (Mock) + string sendChannel = "none"; + if (!string.IsNullOrEmpty(input.Email)) + { + // Mock send email + _logger.LogInformation($"Send Reset Token {resetToken} to Email {input.Email}"); + sendChannel = "email"; + } + else if (!string.IsNullOrEmpty(input.SmsCode)) + { + // Verify Sms Code (Mock) + // Mock send sms + _logger.LogInformation($"Verify SMS Code {input.SmsCode} and generate token"); + sendChannel = "sms"; + } + + return new WeChatResetPasswordOutput + { + ResetToken = resetToken, + ExpireTime = DateTime.Now.AddMinutes(15), + SendChannel = sendChannel + }; + } + + /// + /// AES-128-CBC 解密 + /// + private string DecryptWeChatData(string encryptedData, string sessionKey, string iv) + { + try + { + byte[] encryptedBytes = Convert.FromBase64String(encryptedData); + byte[] keyBytes = Convert.FromBase64String(sessionKey); + byte[] ivBytes = Convert.FromBase64String(iv); + + using var aes = Aes.Create(); + aes.KeySize = 128; + aes.BlockSize = 128; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = keyBytes; + aes.IV = ivBytes; + + using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + using var ms = new MemoryStream(encryptedBytes); + using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); + using var sr = new StreamReader(cs); + return sr.ReadToEnd(); + } + catch (Exception ex) + { + _logger.LogError(ex, "AES Decrypt Error"); + throw Oops.Bah("数据解密失败"); + } + } + + /// + /// 生成自定义 Token + /// + private (string AccessToken, string RefreshToken) GenerateTokens(AuthAccount account) + { long uniqueId = _idGenerator.NewLong(); - var account = value.Value; - string token = JWTEncryption.Encrypt(new Dictionary() + string accessToken = JWTEncryption.Encrypt(new Dictionary() { [AuthClaimsConst.AuthIdKey] = account.Id, [AuthClaimsConst.AccountKey] = account.OAuthId, [AuthClaimsConst.UuidKey] = uniqueId, [AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet }); - // 获取刷新 token - var refreshToken = JWTEncryption.GenerateRefreshToken(token); - // 设置响应报文头 - _httpContextAccessor.HttpContext!.Response.Headers["access-token"] = token; - _httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken; - string url = (await _easyCachingProvider.GetAsync(key2)).Value; - await _easyCachingProvider.RemoveAsync(key); - await _easyCachingProvider.RemoveAsync(key2); - return url; - } - /// - /// 获取用户信息 - /// - /// - [HttpGet] - public async Task UserInfo() - { - long id = _authManager.UserId; - var account = await _accountRepository.AsQueryable().FirstAsync(x => x.Id == id); - if (account == null) - { - throw Oops.Bah("用户不存在"); - } - return new OAuthAccountDetailOutput - { - Id = account.Id, - Avatar = account.Avatar, - NickName = account.Name - }; + // 获取刷新 token + string refreshToken = JWTEncryption.GenerateRefreshToken(accessToken); + + return (accessToken, refreshToken); } } -*/ \ No newline at end of file diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index d6a5b17..2cc4dca 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -1400,19 +1400,95 @@ 失败结果列表 - + - 用户ID + 微信临时授权码 - + - 昵称 + 加密数据 - + - 头像 + 初始向量 + + + + + 原始数据 + + + + + 签名 + + + + + OpenId + + + + + 加密数据 + + + + + 初始向量 + + + + + 原始数据 + + + + + 签名 + + + + + 邀请码 (可选) + + + + + 微信小程序认证接口 + + + + + 登录接口 + + + + + + + 注册接口 + + + + + + + 找回密码接口 + + + + + + + AES-128-CBC 解密 + + + + + 生成自定义 Token diff --git a/YD_Event.Application/applicationsettings.json b/YD_Event.Application/applicationsettings.json index 575cce4..50f2a84 100644 --- a/YD_Event.Application/applicationsettings.json +++ b/YD_Event.Application/applicationsettings.json @@ -101,6 +101,11 @@ "scope": "get_user_info" } }, + // 微信小程序配置 + "WeChat": { + "AppId": "wx1234567890abcdef", + "Secret": "1234567890abcdef1234567890abcdef" + }, // 雪花id 文档:https://github.com/yitter/IdGenerator "SnowId": { "Method": 1, //雪花计算方法(1-漂移算法|2-传统算法),默认1 diff --git a/YD_Event.Core/Entities/AppUser.cs b/YD_Event.Core/Entities/AppUser.cs index e38e6ca..778a681 100644 --- a/YD_Event.Core/Entities/AppUser.cs +++ b/YD_Event.Core/Entities/AppUser.cs @@ -37,6 +37,18 @@ public class AppUser : Entity, ISoftDelete, ICreatedUserId, ICreatedTime, [SugarColumn(IsNullable = true)] public int? CertificationStatus { get; set; } + /// + /// 昵称 (来自微信等第三方) + /// + [SugarColumn(Length = 64, IsNullable = true)] + public string? NickName { get; set; } + + /// + /// 头像 (来自微信等第三方) + /// + [SugarColumn(Length = 512, IsNullable = true)] + public string? Avatar { get; set; } + /// /// 姓名 /// diff --git a/YD_Event.Core/Extensions/ServiceCollectionExtension.cs b/YD_Event.Core/Extensions/ServiceCollectionExtension.cs index d71bd7f..2e2d132 100644 --- a/YD_Event.Core/Extensions/ServiceCollectionExtension.cs +++ b/YD_Event.Core/Extensions/ServiceCollectionExtension.cs @@ -1,4 +1,4 @@ -using YD_Event.Core.Options; +using YD_Event.Core.Options; namespace Easy.Core; @@ -16,6 +16,7 @@ public static class ServiceCollectionExtension service.AddConfigurableOptions(); service.AddConfigurableOptions(); service.AddConfigurableOptions(); + service.AddConfigurableOptions(); return service; } } \ No newline at end of file diff --git a/YD_Event.Core/Options/WeChatOptions.cs b/YD_Event.Core/Options/WeChatOptions.cs new file mode 100644 index 0000000..acfb495 --- /dev/null +++ b/YD_Event.Core/Options/WeChatOptions.cs @@ -0,0 +1,10 @@ +using Furion.ConfigurableOptions; + +namespace YD_Event.Core.Options; + +[OptionsSettings("WeChat")] +public class WeChatOptions : IConfigurableOptions +{ + public string AppId { get; set; } + public string Secret { get; set; } +} diff --git a/YD_Event.Core/Utils/WeChatTool.cs b/YD_Event.Core/Utils/WeChatTool.cs new file mode 100644 index 0000000..f7aa9b9 --- /dev/null +++ b/YD_Event.Core/Utils/WeChatTool.cs @@ -0,0 +1,93 @@ +using System.Net.Http.Json; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Polly; +using Polly.Retry; +using YD_Event.Core.Options; +using Furion.DependencyInjection; +using System.Net.Http; +using System.Threading.Tasks; +using Furion.FriendlyException; + +namespace YD_Event.Core.Utils; + +public class WeChatTool : ITransient +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly WeChatOptions _options; + + public WeChatTool(IHttpClientFactory httpClientFactory, ILogger logger, IOptions options) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _options = options.Value; + } + + /// + /// code换取session_key + /// + /// + /// + public async Task Code2Session(string code) + { + var url = $"https://api.weixin.qq.com/sns/jscode2session?appid={_options.AppId}&secret={_options.Secret}&js_code={code}&grant_type=authorization_code"; + + // 定义重试策略:3秒超时,重试2次 + var retryPolicy = Policy + .Handle() + .Or() + .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(1), (exception, timeSpan, retryCount, context) => + { + _logger.LogWarning($"WeChat API Request Failed. Retry {retryCount}. Error: {exception.Message}"); + }); + + var timeoutPolicy = Policy.TimeoutAsync(3); // 3秒超时 + + var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy); + + try + { + return await policyWrap.ExecuteAsync(async () => + { + var client = _httpClientFactory.CreateClient(); + var response = await client.GetAsync(url); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(content); + + if (result == null || result.ErrCode != 0) + { + _logger.LogError($"WeChat Code2Session Error: {content}"); + throw Oops.Bah($"微信接口调用失败: {result?.ErrMsg ?? "未知错误"}"); + } + + return result; + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "WeChat Code2Session Exception"); + throw Oops.Bah("微信服务暂不可用,请稍后重试"); + } + } +} + +public class WeChatCode2SessionResponse +{ + [JsonProperty("openid")] + public string OpenId { get; set; } + + [JsonProperty("session_key")] + public string SessionKey { get; set; } + + [JsonProperty("unionid")] + public string UnionId { get; set; } + + [JsonProperty("errcode")] + public int ErrCode { get; set; } + + [JsonProperty("errmsg")] + public string ErrMsg { get; set; } +} diff --git a/YD_Event.Core/YD_Event.Core.csproj b/YD_Event.Core/YD_Event.Core.csproj index 923b748..701de1a 100644 --- a/YD_Event.Core/YD_Event.Core.csproj +++ b/YD_Event.Core/YD_Event.Core.csproj @@ -19,8 +19,10 @@ + + diff --git a/YD_Event.Core/YD_Event.Core.xml b/YD_Event.Core/YD_Event.Core.xml index 79e0843..49903eb 100644 --- a/YD_Event.Core/YD_Event.Core.xml +++ b/YD_Event.Core/YD_Event.Core.xml @@ -485,6 +485,16 @@ 认证状态(教练特有):0-未认证,1-已认证 + + + 昵称 (来自微信等第三方) + + + + + 头像 (来自微信等第三方) + + 姓名 @@ -4503,6 +4513,13 @@ 子选项 + + + code换取session_key + + + + 阿里云短信服务实现