using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Settings;
using QYZH.InteractiveMagazine.Models.WeChat;
using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service;
///
/// 微信小程序认证服务实现
/// 登录基于 WxUser 表(微信身份),多用户管理基于 Users 表(角色/子用户)
///
public class WeChatAuthService(
BaseRepository wxUserRepository,
IConfiguration configuration,
ILogger logger,
IPetService petService)
: BaseRepository, 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}";
///
/// 微信小程序登录
/// 流程: code2session → 查找/创建 WxUser → 更新手机号 → 查询 Users 列表 → 生成 Token
///
public async Task LoginAsync(WeChatLoginInput input)
{
logger.LogInformation("微信小程序登录");
if (string.IsNullOrWhiteSpace(input.Code))
throw new BusinessException("微信登录凭证 code 不能为空", 400);
var weChatSettings = GetWeChatSettings();
// 1. 调用微信 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);
// 2. 获取手机号(如果传入了 PhoneCode)
string? phone = null;
if (!string.IsNullOrWhiteSpace(input.PhoneCode))
{
phone = await GetPhoneNumberAsync(weChatSettings, input.PhoneCode);
logger.LogInformation("获取手机号,OpenId: {OpenId}, Phone: {Phone}", wxResponse.OpenId, phone ?? "null");
}
// 3. 查找或创建 WxUser
var wxUser = await wxUserRepository.Context.Queryable()
.Where(w => w.OpenId == wxResponse.OpenId && !w.IsDeleted)
.FirstAsync();
if (wxUser == null)
{
// 首次登录:仅创建 WxUser,不自动创建 User(用户需手动创建)
wxUser = new WxUser
{
Name = $"wx_{wxResponse.OpenId[^8..]}",
AvatarUrl = string.Empty,
OpenId = wxResponse.OpenId,
UnionId = wxResponse.UnionId,
Phone = phone,
Status = 1,
IsDeleted = false,
CreatedBy = "WeChat",
CreatedAt = DateTime.Now,
UpdatedBy = "WeChat",
UpdatedAt = DateTime.Now
};
var wxUserId = await wxUserRepository.Insertable(wxUser).ExecuteReturnIdentityAsync();
wxUser.Id = wxUserId;
logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUserId, wxResponse.OpenId);
}
else
{
// 非首次登录:更新 UnionId 和手机号
if (!string.IsNullOrWhiteSpace(wxResponse.UnionId) && string.IsNullOrWhiteSpace(wxUser.UnionId))
{
await wxUserRepository.Context.Updateable()
.SetColumns(w => w.UnionId == wxResponse.UnionId)
.Where(w => w.Id == wxUser.Id)
.ExecuteCommandAsync();
wxUser.UnionId = wxResponse.UnionId;
}
if (!string.IsNullOrWhiteSpace(phone))
{
await wxUserRepository.Context.Updateable()
.SetColumns(w => w.Phone == phone)
.Where(w => w.Id == wxUser.Id)
.ExecuteCommandAsync();
wxUser.Phone = phone;
logger.LogInformation("更新 WxUser 手机号,WxUserId: {WxUserId}, Phone: {Phone}", wxUser.Id, phone);
}
}
// 查询该 WxUser 下所有 Users(首次登录时为空列表)
var users = await wxUserRepository.Context.Queryable()
.Where(u => u.WxUserId == wxUser.Id && !u.IsDeleted)
.ToListAsync();
logger.LogInformation("微信登录成功,WxUserId: {WxUserId} 下存在 {Count} 个用户", wxUser.Id, users.Count);
return await BuildLoginOutputAsync(wxUser, users);
}
///
/// 微信小程序快捷登录(通过 OpenId 直接登录)
///
public async Task QuickLoginAsync(WeChatQuickLoginInput input)
{
logger.LogInformation("微信快捷登录,OpenId: {OpenId}", input.OpenId);
if (string.IsNullOrWhiteSpace(input.OpenId))
throw new BusinessException("OpenId 不能为空", 400);
var wxUser = await wxUserRepository.Context.Queryable()
.Where(w => w.OpenId == input.OpenId && !w.IsDeleted)
.FirstAsync();
if (wxUser == null)
{
logger.LogWarning("快捷登录失败,OpenId: {OpenId} 下无 WxUser", input.OpenId);
throw new BusinessException("未找到该微信账号关联的用户,请先完成注册", 404);
}
var users = await wxUserRepository.Context.Queryable()
.Where(u => u.WxUserId == wxUser.Id && !u.IsDeleted)
.ToListAsync();
logger.LogInformation("快捷登录成功,WxUserId: {WxUserId}, {Count} 个用户", wxUser.Id, users.Count);
return await BuildLoginOutputAsync(wxUser, users);
}
///
/// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token)
///
public async Task SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input)
{
logger.LogInformation("切换用户,WxUserId: {WxUserId}, 当前 UserId: {CurrentUserId}, 目标 UserId: {TargetUserId}",
wxUserId, currentUserId, input.UserId);
// 查询目标用户
var targetUser = await wxUserRepository.Context.Queryable()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
if (targetUser == null)
throw new BusinessException("目标用户不存在", 404);
// 校验目标用户属于同一 WxUser
if (targetUser.WxUserId != wxUserId)
{
logger.LogWarning("切换用户失败,WxUserId 不匹配,当前: {Current}, 目标: {Target}", wxUserId, targetUser.WxUserId);
throw new BusinessException("无法切换到该用户", 403);
}
if (targetUser.Status == (int)UserStatusEnum.Disabled)
throw new BusinessException("目标账号已被禁用", 403);
// 更新 IsLastOnline(清除所有,设置目标为 true)
await wxUserRepository.Context.Updateable()
.SetColumns(u => u.IsLastOnline == false)
.Where(u => u.WxUserId == wxUserId && !u.IsDeleted)
.ExecuteCommandAsync();
await wxUserRepository.Context.Updateable()
.SetColumns(u => u.IsLastOnline == true)
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync();
logger.LogInformation("IsLastOnline 已更新,目标用户 {UserId} 设为 true", input.UserId);
// 重新查询目标用户获取最新数据
var refreshedUser = await wxUserRepository.Context.Queryable()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
// 生成新 JWT Token(WxUserId + 新 UserId)
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken(wxUserId, refreshedUser.Id, refreshedUser.Name ?? string.Empty, jwtSettings);
// 清除旧 Redis Token,写入新 Token
await RedisHelper.DelAsync($"{TokenKeyPrefix}:{currentUserId}");
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{refreshedUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatSwitchUserOutput
{
Token = token,
User = MapUserToOutput(refreshedUser)
};
}
///
/// 获取当前 WxUser 下所有用户列表
///
public async Task> GetUsersAsync(long wxUserId)
{
var users = await wxUserRepository.Context.Queryable()
.Where(u => u.WxUserId == wxUserId && !u.IsDeleted)
.OrderBy(u => u.IsLastOnline, SqlSugar.OrderByType.Desc)
.OrderBy(u => u.CreatedAt, SqlSugar.OrderByType.Desc)
.ToListAsync();
return users.Select(MapUserToOutput).ToList();
}
///
/// 在当前 WxUser 下新增用户(wxUserId 来自 JWT)
///
public async Task CreateUserAsync(long wxUserId, CreateChildUserInput input)
{
logger.LogInformation("新增用户,WxUserId: {WxUserId}, Name: {Name}", wxUserId, input.Name);
if (string.IsNullOrWhiteSpace(input.Name))
throw new BusinessException("昵称不能为空", 400);
// 校验 WxUser 是否存在
var wxUser = await wxUserRepository.Context.Queryable()
.Where(w => w.Id == wxUserId)
.FirstAsync();
if (wxUser == null)
throw new BusinessException("微信用户不存在", 404);
// 创建新用户
var newUser = new Users
{
WxUserId = wxUserId,
Name = input.Name.Trim(),
AvatarUrl = input.AvatarUrl ?? string.Empty,
Type = UsersTypeEnum.Normal,
GrowthPoints = 0,
Points = 0,
IsLastOnline = false,
Status = (int)UserStatusEnum.Active,
IsDeleted = false,
CreatedBy = wxUserId.ToString(),
CreatedAt = DateTime.Now,
UpdatedBy = wxUserId.ToString(),
UpdatedAt = DateTime.Now
};
await wxUserRepository.Context.Insertable(newUser).ExecuteReturnIdentityAsync();
logger.LogInformation("新用户创建成功,UserId: {UserId}, WxUserId: {WxUserId}, Name: {Name}",
newUser.Id, wxUserId, input.Name);
// 为新用户创建默认宠物
try { await petService.CreateDefaultPetAsync(newUser.Id); }
catch (Exception ex) { logger.LogError(ex, "新用户创建默认宠物失败,UserId: {UserId}", newUser.Id); }
return MapUserToOutput(newUser);
}
///
/// 修改家长名字(WxUser.Name)
///
public async Task UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input)
{
logger.LogInformation("修改家长名字,WxUserId: {WxUserId}, NewName: {Name}", wxUserId, input.Name);
if (string.IsNullOrWhiteSpace(input.Name))
throw new BusinessException("名字不能为空", 400);
var wxUser = await wxUserRepository.Context.Queryable()
.Where(w => w.Id == wxUserId && !w.IsDeleted)
.FirstAsync();
if (wxUser == null)
throw new BusinessException("微信用户不存在", 404);
await wxUserRepository.Context.Updateable()
.SetColumns(w => w.Name == input.Name.Trim())
.SetColumns(w => w.UpdatedAt == DateTime.Now)
.SetColumns(w => w.UpdatedBy == wxUserId.ToString())
.Where(w => w.Id == wxUserId)
.ExecuteCommandAsync();
wxUser.Name = input.Name.Trim();
logger.LogInformation("家长名字修改成功,WxUserId: {WxUserId}, NewName: {Name}", wxUserId, input.Name);
return new WxUserInfoOutput
{
Id = wxUser.Id,
OpenId = wxUser.OpenId,
UnionId = wxUser.UnionId,
Name = wxUser.Name,
AvatarUrl = wxUser.AvatarUrl,
Phone = wxUser.Phone
};
}
#region 私有辅助方法
///
/// 构建登录输出(JWT 同时携带 WxUserId 和当前激活 UserId)
///
private async Task BuildLoginOutputAsync(WxUser wxUser, List users)
{
var activeUser = users.FirstOrDefault(u => u.IsLastOnline) ?? users.FirstOrDefault();
var userId = activeUser?.Id ?? 0L;
var userName = activeUser?.Name ?? wxUser.Name ?? wxUser.OpenId;
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken(wxUser.Id, userId, userName, jwtSettings);
await RedisHelper.SetAsync($"{TokenKeyPrefix}:{userId}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
return new WeChatLoginOutput
{
Token = token,
CurrentUserId = userId,
WxUser = new WxUserInfoOutput
{
Id = wxUser.Id,
OpenId = wxUser.OpenId,
UnionId = wxUser.UnionId,
Name = wxUser.Name,
AvatarUrl = wxUser.AvatarUrl,
Phone = wxUser.Phone
},
Users = users.Select(MapUserToOutput).ToList()
};
}
///
/// 调用微信 code2session 接口
///
private async Task CallCode2SessionAsync(WeChatSettings settings, string code)
{
var url = string.Format(Code2SessionUrl, settings.AppId, settings.AppSecret, code);
try
{
return await HttpHelper.GetAsync(url);
}
catch (Exception ex)
{
logger.LogError(ex, "调用微信 code2session 接口异常,URL: {Url}", url);
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
}
}
///
/// 获取微信 access_token(带 Redis 缓存)
///
private async Task GetAccessTokenAsync(WeChatSettings settings)
{
var cachedToken = await RedisHelper.GetAsync(AccessTokenCacheKey);
if (!string.IsNullOrWhiteSpace(cachedToken))
return cachedToken;
var url = string.Format(GetAccessTokenUrl, settings.AppId, settings.AppSecret);
var response = await HttpHelper.GetAsync(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);
}
var expiresIn = response.ExpiresIn > 300 ? response.ExpiresIn - 300 : response.ExpiresIn;
await RedisHelper.SetAsync(AccessTokenCacheKey, response.AccessToken, TimeSpan.FromSeconds(expiresIn));
logger.LogInformation("获取微信 access_token 成功,有效期: {ExpiresIn} 秒", expiresIn);
return response.AccessToken;
}
///
/// 通过 phone_code 获取微信用户手机号
///
private async Task GetPhoneNumberAsync(WeChatSettings settings, string phoneCode)
{
try
{
var accessToken = await GetAccessTokenAsync(settings);
var url = string.Format(GetPhoneNumberUrl, accessToken);
var response = await HttpHelper.PostAsync(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;
}
}
///
/// Users 实体映射为 WxUserOutput
///
private static WxUserOutput MapUserToOutput(Users user)
{
return new WxUserOutput
{
Id = (long)user.Id,
WxUserId = user.WxUserId,
NickName = user.Name,
AvatarUrl = user.AvatarUrl,
Points = user.Points,
GrowthPoints = user.GrowthPoints,
Type = user.Type.ToString(),
Status = user.Status.ToString(),
IsLastOnline = user.IsLastOnline,
CreatedAt = user.CreatedAt
};
}
private WeChatSettings GetWeChatSettings()
{
var settings = configuration.GetSection("WeChatSettings").Get();
if (settings == null || string.IsNullOrWhiteSpace(settings.AppId) || string.IsNullOrWhiteSpace(settings.AppSecret))
{
logger.LogError("微信配置不完整,请检查 appsettings.json 中的 WeChatSettings 节点");
throw new BusinessException("微信配置不完整,请联系系统管理员", 500);
}
return settings;
}
private JwtSettings GetJwtSettings()
{
var jwtSettings = configuration.GetSection("JwtSettings").Get()
?? 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;
}
#endregion
}