微信登录

This commit is contained in:
马梓彭 2026-03-14 06:13:06 +08:00
parent 735fd67b75
commit 558ae72e50
11 changed files with 619 additions and 154 deletions

View File

@ -0,0 +1,133 @@
using System.ComponentModel.DataAnnotations;
namespace YD_Event.Application.Client.Dtos;
public class WeChatLoginInput
{
/// <summary>
/// 微信临时授权码
/// </summary>
[Required(ErrorMessage = "code不能为空")]
public string Code { get; set; }
/// <summary>
/// 加密数据
/// </summary>
[Required(ErrorMessage = "encryptedData不能为空")]
public string EncryptedData { get; set; }
/// <summary>
/// 初始向量
/// </summary>
[Required(ErrorMessage = "iv不能为空")]
public string Iv { get; set; }
/// <summary>
/// 原始数据
/// </summary>
[Required(ErrorMessage = "rawData不能为空")]
public string RawData { get; set; }
/// <summary>
/// 签名
/// </summary>
[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
{
/// <summary>
/// OpenId
/// </summary>
[Required(ErrorMessage = "openid不能为空")]
public string Openid { get; set; }
/// <summary>
/// 加密数据
/// </summary>
[Required(ErrorMessage = "encryptedData不能为空")]
public string EncryptedData { get; set; }
/// <summary>
/// 初始向量
/// </summary>
[Required(ErrorMessage = "iv不能为空")]
public string Iv { get; set; }
/// <summary>
/// 原始数据
/// </summary>
[Required(ErrorMessage = "rawData不能为空")]
public string RawData { get; set; }
/// <summary>
/// 签名
/// </summary>
[Required(ErrorMessage = "signature不能为空")]
public string Signature { get; set; }
/// <summary>
/// 邀请码 (可选)
/// </summary>
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; }
}

View File

@ -1,18 +0,0 @@
namespace YD_Event.Application.Client.Dtos;
public class OAuthAccountDetailOutput
{
/// <summary>
/// 用户ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 昵称
/// </summary>
public string NickName { get; set; }
/// <summary>
/// 头像
/// </summary>
public string Avatar { get; set; }
}

View File

@ -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;
/// <summary>
/// 第三方授权登录
/// 微信小程序认证接口
/// </summary>
[ApiDescriptionSettings("微信小程序接口")]
[Route("api/v1/mini-program/auth")]
public class OAuthController : IDynamicApiController
{
/// <summary>
/// 第三方授权缓存
/// </summary>
private const string OAuthKey = "oauth.";
/// <summary>
/// 授权成功后回调页面缓存键
/// </summary>
private const string OAuthRedirectKey = "oauth.redirect.";
private readonly QQOAuth _qqoAuth;
private readonly AuthManager _authManager;
private readonly WeChatTool _weChatTool;
private readonly ISqlSugarRepository<AuthAccount> _accountRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
private readonly ISqlSugarRepository<SysRole> _roleRepository;
private readonly ISqlSugarRepository<SysUserRole> _userRoleRepository;
private readonly IEasyCachingProvider _easyCachingProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IIdGenerator _idGenerator;
private readonly ILogger<OAuthController> _logger;
public OAuthController(QQOAuth qqoAuth,
AuthManager authManager,
private const string SessionKeyCachePrefix = "wechat_session_";
public OAuthController(
WeChatTool weChatTool,
ISqlSugarRepository<AuthAccount> accountRepository,
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
ISqlSugarRepository<SysRole> roleRepository,
ISqlSugarRepository<SysUserRole> userRoleRepository,
IEasyCachingProvider easyCachingProvider,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator)
IIdGenerator idGenerator,
ILogger<OAuthController> logger)
{
_qqoAuth = qqoAuth;
_authManager = authManager;
_weChatTool = weChatTool;
_accountRepository = accountRepository;
_appUserRepository = appUserRepository;
_roleRepository = roleRepository;
_userRoleRepository = userRoleRepository;
_easyCachingProvider = easyCachingProvider;
_httpContextAccessor = httpContextAccessor;
_idGenerator = idGenerator;
_logger = logger;
}
/// <summary>
/// 获取授权地址
/// 登录接口
/// </summary>
/// <param name="type">授权登录类型</param>
/// <param name="referer">回跳地址</param>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet("{type}")]
[HttpPost("login")]
[AllowAnonymous]
public async Task<string> Get(string type, [FromQuery][Required(ErrorMessage = "缺少参数")] string referer)
public async Task<WeChatLoginOutput> 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;
}
/// <summary>
/// 授权回调
/// 注册接口
/// </summary>
/// <param name="type">授权类型</param>
/// <param name="code"></param>
/// <param name="state">缓存唯一ID</param>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet("{type}/callback")]
[HttpPost("register")]
[AllowAnonymous]
public async Task<IActionResult> Callback(string type, [FromQuery] string code, [FromQuery] string state)
[UnitOfWork]
public async Task<WeChatRegisterOutput> 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<string>(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<WeChatUserInfo>(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<string>($"{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
};
}
/// <summary>
/// 登录
/// 找回密码接口
/// </summary>
/// <param name="code"></param>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("login/{code}")]
[HttpPost("reset-password")]
[AllowAnonymous]
public async Task<string> Login(string code)
public async Task<WeChatResetPasswordOutput> ResetPassword([FromBody] WeChatResetPasswordInput input)
{
string key = $"{OAuthKey}{code}", key2 = $"{OAuthRedirectKey}{code}";
var value = await _easyCachingProvider.GetAsync<AuthAccount>(key);
if (!value.HasValue)
// 1. 获取 session_key
var cacheKey = $"{SessionKeyCachePrefix}{input.Openid}";
var sessionKeyCache = await _easyCachingProvider.GetAsync<string>(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<WeChatUserInfo>(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
};
}
/// <summary>
/// AES-128-CBC 解密
/// </summary>
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("数据解密失败");
}
}
/// <summary>
/// 生成自定义 Token
/// </summary>
private (string AccessToken, string RefreshToken) GenerateTokens(AuthAccount account)
{
long uniqueId = _idGenerator.NewLong();
var account = value.Value;
string token = JWTEncryption.Encrypt(new Dictionary<string, object>()
string accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
{
[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<string>(key2)).Value;
await _easyCachingProvider.RemoveAsync(key);
await _easyCachingProvider.RemoveAsync(key2);
return url;
}
/// <summary>
/// 获取用户信息
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<OAuthAccountDetailOutput> 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);
}
}
*/

View File

@ -1400,19 +1400,95 @@
失败结果列表
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.Id">
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Code">
<summary>
用户ID
微信临时授权码
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.NickName">
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.EncryptedData">
<summary>
昵称
加密数据
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.OAuthAccountDetailOutput.Avatar">
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Iv">
<summary>
头像
初始向量
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.RawData">
<summary>
原始数据
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatLoginInput.Signature">
<summary>
签名
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Openid">
<summary>
OpenId
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.EncryptedData">
<summary>
加密数据
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Iv">
<summary>
初始向量
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.RawData">
<summary>
原始数据
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.Signature">
<summary>
签名
</summary>
</member>
<member name="P:YD_Event.Application.Client.Dtos.WeChatRegisterInput.InviteCode">
<summary>
邀请码 (可选)
</summary>
</member>
<member name="T:YD_Event.Application.Client.OAuthController">
<summary>
微信小程序认证接口
</summary>
</member>
<member name="M:YD_Event.Application.Client.OAuthController.Login(YD_Event.Application.Client.Dtos.WeChatLoginInput)">
<summary>
登录接口
</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Client.OAuthController.Register(YD_Event.Application.Client.Dtos.WeChatRegisterInput)">
<summary>
注册接口
</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Client.OAuthController.ResetPassword(YD_Event.Application.Client.Dtos.WeChatResetPasswordInput)">
<summary>
找回密码接口
</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="M:YD_Event.Application.Client.OAuthController.DecryptWeChatData(System.String,System.String,System.String)">
<summary>
AES-128-CBC 解密
</summary>
</member>
<member name="M:YD_Event.Application.Client.OAuthController.GenerateTokens(YD_Event.Core.Entities.AuthAccount)">
<summary>
生成自定义 Token
</summary>
</member>
<member name="T:YD_Event.Application.Coupon.CouponService">

View File

@ -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

View File

@ -37,6 +37,18 @@ public class AppUser : Entity<long>, ISoftDelete, ICreatedUserId, ICreatedTime,
[SugarColumn(IsNullable = true)]
public int? CertificationStatus { get; set; }
/// <summary>
/// 昵称 (来自微信等第三方)
/// </summary>
[SugarColumn(Length = 64, IsNullable = true)]
public string? NickName { get; set; }
/// <summary>
/// 头像 (来自微信等第三方)
/// </summary>
[SugarColumn(Length = 512, IsNullable = true)]
public string? Avatar { get; set; }
/// <summary>
/// 姓名
/// </summary>

View File

@ -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<SnowIdOptions>();
service.AddConfigurableOptions<SmsOptions>();
service.AddConfigurableOptions<AliyunOcrOptions>();
service.AddConfigurableOptions<WeChatOptions>();
return service;
}
}

View File

@ -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; }
}

View File

@ -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<WeChatTool> _logger;
private readonly WeChatOptions _options;
public WeChatTool(IHttpClientFactory httpClientFactory, ILogger<WeChatTool> logger, IOptions<WeChatOptions> options)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_options = options.Value;
}
/// <summary>
/// code换取session_key
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public async Task<WeChatCode2SessionResponse> 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<HttpRequestException>()
.Or<TaskCanceledException>()
.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<WeChatCode2SessionResponse>(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; }
}

View File

@ -19,8 +19,10 @@
<PackageReference Include="Hashids.net" Version="1.7.0" />
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.2" />
<PackageReference Include="log4net" Version="3.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="MrHuo.OAuth.QQ" Version="1.1.1" />
<PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.2.0" />
<PackageReference Include="Polly" Version="8.6.6" />
<PackageReference Include="QuestPDF" Version="2025.12.1" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="DocumentFormat.OpenXml" Version="2.20.0" />

View File

@ -485,6 +485,16 @@
认证状态(教练特有):0-未认证,1-已认证
</summary>
</member>
<member name="P:YD_Event.Core.Entities.AppUser.NickName">
<summary>
昵称 (来自微信等第三方)
</summary>
</member>
<member name="P:YD_Event.Core.Entities.AppUser.Avatar">
<summary>
头像 (来自微信等第三方)
</summary>
</member>
<member name="P:YD_Event.Core.Entities.AppUser.Name">
<summary>
姓名
@ -4503,6 +4513,13 @@
子选项
</summary>
</member>
<member name="M:YD_Event.Core.Utils.WeChatTool.Code2Session(System.String)">
<summary>
code换取session_key
</summary>
<param name="code"></param>
<returns></returns>
</member>
<member name="T:YD_Event.Application.Sms.AliyunSmsService">
<summary>
阿里云短信服务实现