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.Dto; 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 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 不能为空", ResultCode.BAD_REQUEST); 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}", ResultCode.BAD_REQUEST); } 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 }; await wxUserRepository.InsertAsync(wxUser); logger.LogInformation("WxUser 创建成功,WxUserId: {WxUserId}, OpenId: {OpenId}", wxUser.Id, 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 不能为空", ResultCode.BAD_REQUEST); 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("未找到该微信账号关联的用户,请先完成注册", ResultCode.NOT_FOUND); } 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("目标用户不存在", ResultCode.NOT_FOUND); // 校验目标用户属于同一 WxUser if (targetUser.WxUserId != wxUserId) { logger.LogWarning("切换用户失败,WxUserId 不匹配,当前: {Current}, 目标: {Target}", wxUserId, targetUser.WxUserId); throw new BusinessException("无法切换到该用户", ResultCode.FORBIDDEN); } if (targetUser.Status == (int)UserStatusEnum.Disabled) throw new BusinessException("目标账号已被禁用", ResultCode.FORBIDDEN); // 更新 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(JwtHelper.BuildWeChatTokenKey(wxUserId, currentUserId)); await RedisHelper.SetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, 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("昵称不能为空", ResultCode.BAD_REQUEST); // 校验 WxUser 是否存在 var wxUser = await wxUserRepository.Context.Queryable() .Where(w => w.Id == wxUserId) .FirstAsync(); if (wxUser == null) throw new BusinessException("微信用户不存在", ResultCode.NOT_FOUND); // 创建新用户 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("名字不能为空", ResultCode.BAD_REQUEST); var wxUser = await wxUserRepository.Context.Queryable() .Where(w => w.Id == wxUserId && !w.IsDeleted) .FirstAsync(); if (wxUser == null) throw new BusinessException("微信用户不存在", ResultCode.NOT_FOUND); 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(JwtHelper.BuildWeChatTokenKey(wxUser.Id, 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("微信服务请求失败,请稍后重试", ResultCode.GLOBAL_ERROR); } } /// /// 获取微信 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("微信服务请求失败,请稍后重试", ResultCode.GLOBAL_ERROR); } 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("微信配置不完整,请联系系统管理员", ResultCode.GLOBAL_ERROR); } 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 配置不完整", ResultCode.GLOBAL_ERROR); return jwtSettings; } #endregion }