Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs

394 lines
16 KiB
C#
Raw Normal View History

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.IService.Dto;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Settings;
using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 微信小程序认证服务实现
/// </summary>
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger, IPetService petService) : BaseRepository<Users>, IWeChatAuthService
{
private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
private const string AccessTokenCacheKey = "InteractiveMagazine:WeChat:AccessToken";
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
private const string GetAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
private const string GetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={0}";
/// <summary>
/// 微信小程序登录(首次创建用户,非首次直接登录)
/// </summary>
public async Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input)
{
logger.LogInformation("微信小程序登录");
if (string.IsNullOrWhiteSpace(input.Code))
{
throw new BusinessException("微信登录凭证 code 不能为空", 400);
}
var weChatSettings = GetWeChatSettings();
// 调用微信 code2session 接口
var wxResponse = await CallCode2SessionAsync(weChatSettings, input.Code);
if (wxResponse == null || wxResponse.ErrCode != 0 || string.IsNullOrWhiteSpace(wxResponse.OpenId))
{
var errMsg = wxResponse?.ErrMsg ?? "未知错误";
logger.LogWarning("微信 code2session 接口调用失败errcode: {ErrCode}, errmsg: {ErrMsg}", wxResponse?.ErrCode, errMsg);
throw new BusinessException($"微信登录失败:{errMsg}", 400);
}
logger.LogInformation("微信 code2session 成功OpenId: {OpenId}", wxResponse.OpenId);
// 查询该 OpenId 下的所有用户
var users = await usersRepository.Context.Queryable<Users>()
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
.ToListAsync();
if (users.Count == 0)
{
// 首次登录,获取手机号(如果传入了 PhoneCode
string? phone = null;
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
{
phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
logger.LogInformation("获取手机号成功OpenId: {OpenId}, Phone: {Phone}", wxResponse.OpenId, phone);
}
// 创建新用户
var newUser = new Users
{
Name = $"wx_{wxResponse.OpenId[^8..]}",
OpenId = wxResponse.OpenId,
UnionId = wxResponse.UnionId,
Phone = phone,
Type = "Normal",
Status = "Active",
GrowthPoints = 0,
Points = 0,
IsLastOnline = true
};
var insertResult = await usersRepository.Insertable(newUser).ExecuteReturnIdentityAsync();
if (insertResult <= 0)
{
logger.LogError("创建微信用户失败OpenId: {OpenId}", wxResponse.OpenId);
throw new BusinessException("创建用户失败,请稍后重试", 500);
}
newUser.Id = insertResult;
users.Add(newUser);
logger.LogInformation("微信新用户创建成功UserId: {UserId}, OpenId: {OpenId}", newUser.Id, wxResponse.OpenId);
// 为新用户创建默认宠物(未激活状态)
try
{
await petService.CreateDefaultPetAsync(newUser.Id);
}
catch (Exception ex)
{
logger.LogError(ex, "新用户创建默认宠物失败UserId: {UserId}", newUser.Id);
// 宠物创建失败不阻断注册流程
}
}
else
{
// 非首次登录,如果传入了 PhoneCode 则更新该 OpenId 下所有用户的手机号
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
{
var phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
if (!string.IsNullOrWhiteSpace(phone))
{
await usersRepository.Context.Updateable<Users>()
.SetColumns(u => u.Phone == phone)
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
.ExecuteCommandAsync();
foreach (var u in users)
{
u.Phone = phone;
}
logger.LogInformation("更新 OpenId: {OpenId} 下所有用户手机号成功Phone: {Phone}", wxResponse.OpenId, phone);
}
}
logger.LogInformation("微信登录成功OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
}
// 构建登录输出
return await BuildLoginOutputAsync(wxResponse.OpenId, users);
}
/// <summary>
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在)
/// </summary>
public async Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input)
{
logger.LogInformation("微信快捷登录OpenId: {OpenId}", input.OpenId);
if (string.IsNullOrWhiteSpace(input.OpenId))
{
throw new BusinessException("OpenId 不能为空", 400);
}
// 查询该 OpenId 下的所有用户
var users = await usersRepository.Context.Queryable<Users>()
.Where(u => u.OpenId == input.OpenId && !u.IsDeleted)
.ToListAsync();
if (users.Count == 0)
{
logger.LogWarning("快捷登录失败OpenId: {OpenId} 下无用户", input.OpenId);
throw new BusinessException("未找到该微信账号关联的用户,请先完成注册", 404);
}
logger.LogInformation("快捷登录成功OpenId: {OpenId} 下存在 {Count} 个用户", input.OpenId, users.Count);
return await BuildLoginOutputAsync(input.OpenId, users);
}
/// <summary>
/// 切换用户(同一 OpenId 下切换身份)
/// </summary>
/// <param name="currentUserId">当前登录用户ID</param>
/// <param name="input">切换用户输入</param>
/// <returns>切换结果(含新 Token 和目标用户详情)</returns>
public async Task<WeChatSwitchUserOutput> SwitchUserAsync(long currentUserId, WeChatSwitchUserInput input)
{
logger.LogInformation("切换用户当前用户ID: {CurrentUserId}, 目标用户ID: {TargetUserId}", currentUserId, input.UserId);
// 获取当前用户,验证身份并获取 OpenId
var currentUser = await usersRepository.GetByIdAsync(currentUserId);
if (currentUser == null)
{
throw new BusinessException("当前用户不存在", 404);
}
var openId = currentUser.OpenId;
// 查询目标用户
var targetUser = await usersRepository.GetByIdAsync(input.UserId);
if (targetUser == null)
{
throw new BusinessException("目标用户不存在", 404);
}
// 校验目标用户与当前用户属于同一 OpenId
if (targetUser.OpenId != openId)
{
logger.LogWarning("切换用户失败,目标用户 OpenId 不匹配,当前: {CurrentOpenId}, 目标: {TargetOpenId}", openId, targetUser.OpenId);
throw new BusinessException("无法切换到该用户", 403);
}
if (targetUser.Status == "Disabled")
{
throw new BusinessException("目标账号已被禁用", 403);
}
// 更新 IsLastOnline目标用户设为 true同 OpenId 下其他用户设为 false
await usersRepository.Context.Updateable<Users>()
.SetColumns(u => u.IsLastOnline == false)
.Where(u => u.OpenId == openId && !u.IsDeleted)
.ExecuteCommandAsync();
await usersRepository.Context.Updateable<Users>()
.SetColumns(u => u.IsLastOnline == true)
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync();
logger.LogInformation("IsLastOnline 已更新,目标用户 {UserId} 设为 true", input.UserId);
// TODO: 预留扩展逻辑 —— 加载用户详情、用户关联信息等
// var userDetail = await LoadUserDetailAsync(targetUser.Id);
// var userRelations = await LoadUserRelationsAsync(targetUser.Id);
// 重新查询目标用户以获取最新数据(含 IsLastOnline
var refreshedUser = await usersRepository.GetByIdAsync(input.UserId);
// 为目标用户生成新的 JWT Token
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken((long)refreshedUser.Id, refreshedUser.Name, jwtSettings);
// 清除旧用户 Token 缓存,写入新用户 Token 缓存
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{currentUserId}");
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatSwitchUserOutput
{
Token = token,
User = MapUserToOutput(refreshedUser)
};
}
/// <summary>
/// 调用微信 code2session 接口
/// </summary>
private async Task<WxCode2SessionResponse?> CallCode2SessionAsync(WeChatSettings settings, string code)
{
var url = string.Format(Code2SessionUrl, settings.AppId, settings.AppSecret, code);
try
{
return await HttpHelper.GetAsync<WxCode2SessionResponse>(url);
}
catch (Exception ex)
{
logger.LogError(ex, "调用微信 code2session 接口异常URL: {Url}", url);
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
}
}
/// <summary>
/// 构建登录输出(生成 Token + 映射用户列表)
/// </summary>
private async Task<WeChatLoginOutput> BuildLoginOutputAsync(string openId, List<Users> users)
{
// 优先使用 IsLastOnline 的用户,否则取第一个
var primaryUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.First();
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
// 缓存 Token 到 Redis
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
var userOutputs = users.Select(MapUserToOutput).ToList();
return new WeChatLoginOutput
{
Token = token,
OpenId = openId,
Users = userOutputs
};
}
/// <summary>
/// 获取微信 access_token带 Redis 缓存)
/// </summary>
private async Task<string> GetAccessTokenAsync(WeChatSettings settings)
{
// 先从 Redis 缓存获取
var cachedToken = await RedisHelper.StringGetAsync(AccessTokenCacheKey);
if (!string.IsNullOrWhiteSpace(cachedToken))
{
return cachedToken;
}
// 缓存未命中,调用微信接口获取
var url = string.Format(GetAccessTokenUrl, settings.AppId, settings.AppSecret);
var response = await HttpHelper.GetAsync<WxAccessTokenResponse>(url);
if (response == null || response.ErrCode != 0 || string.IsNullOrWhiteSpace(response.AccessToken))
{
var errMsg = response?.ErrMsg ?? "未知错误";
logger.LogError("获取微信 access_token 失败errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
}
// 缓存 access_token提前 5 分钟过期(微信默认 7200 秒)
var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn;
await RedisHelper.StringSetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn));
logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn);
return response.AccessToken;
}
/// <summary>
/// 通过 phone_code 获取微信用户手机号
/// </summary>
private async Task<string?> GetPhoneNumberAsync(WeChatSettings settings, string phoneCode)
{
try
{
var accessToken = await GetAccessTokenAsync(settings);
var url = string.Format(GetPhoneNumberUrl, accessToken);
var response = await HttpHelper.PostAsync<WxPhoneNumberResponse>(url, new { code = phoneCode });
if (response == null || response.ErrCode != 0 || response.PhoneInfo == null)
{
var errMsg = response?.ErrMsg ?? "未知错误";
logger.LogWarning("获取手机号失败errcode: {ErrCode}, errmsg: {ErrMsg}", response?.ErrCode, errMsg);
// 获取手机号失败不阻断登录流程,仅记录日志
return null;
}
return response.PhoneInfo.PurePhoneNumber ?? response.PhoneInfo.PhoneNumber;
}
catch (Exception ex)
{
logger.LogError(ex, "调用微信获取手机号接口异常");
// 获取手机号失败不阻断登录流程
return null;
}
}
/// <summary>
/// 用户实体映射为输出 DTO
/// </summary>
private static WxUserOutput MapUserToOutput(Users user)
{
return new WxUserOutput
{
Id = (long)user.Id,
OpenId = user.OpenId,
UnionId = user.UnionId,
NickName = user.Name,
AvatarUrl = user.AvatarUrl,
Phone = user.Phone,
Points = user.Points,
GrowthPoints = user.GrowthPoints,
Type = user.Type,
Status = user.Status,
IsLastOnline = user.IsLastOnline,
CreatedAt = user.CreatedAt,
UpdatedAt = user.UpdatedAt,
CreatedBy = user.CreatedBy,
UpdatedBy = user.UpdatedBy
};
}
/// <summary>
/// 获取微信配置
/// </summary>
private WeChatSettings GetWeChatSettings()
{
var settings = configuration.GetSection("WeChatSettings").Get<WeChatSettings>();
if (settings == null || string.IsNullOrWhiteSpace(settings.AppId) || string.IsNullOrWhiteSpace(settings.AppSecret))
{
logger.LogError("微信配置不完整,请检查 appsettings.json 中的 WeChatSettings 节点");
throw new BusinessException("微信配置不完整,请联系系统管理员", 500);
}
return settings;
}
/// <summary>
/// 获取 JWT 配置
/// </summary>
private JwtSettings GetJwtSettings()
{
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()
?? new JwtSettings
{
Issuer = "QYZH.InteractiveMagazine",
Audience = "QYZH.InteractiveMagazine",
SecretKey = "your-256-bit-secret-key-here-change-in-production",
ExpiryMinutes = 120
};
if (string.IsNullOrWhiteSpace(jwtSettings.SecretKey))
{
throw new BusinessException("JWT 配置不完整", 500);
}
return jwtSettings;
}
}