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
+
+
+
+
阿里云短信服务实现