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.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; /// /// 微信小程序认证服务实现 /// public class WeChatAuthService(BaseRepository usersRepository, 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}"; /// /// 微信小程序登录(首次创建用户,非首次直接登录) /// public async Task 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() .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 = UsersTypeEnum.Normal, Status = UserStatusEnum.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() .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); } /// /// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在) /// public async Task 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() .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); } /// /// 切换用户(同一 OpenId 下切换身份) /// /// 当前登录用户ID /// 切换用户输入 /// 切换结果(含新 Token 和目标用户详情) public async Task 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 == UserStatusEnum.Disabled) { throw new BusinessException("目标账号已被禁用", 403); } // 更新 IsLastOnline:目标用户设为 true,同 OpenId 下其他用户设为 false await usersRepository.Context.Updateable() .SetColumns(u => u.IsLastOnline == false) .Where(u => u.OpenId == openId && !u.IsDeleted) .ExecuteCommandAsync(); await usersRepository.Context.Updateable() .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) }; } /// /// 调用微信 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); } } /// /// 构建登录输出(生成 Token + 映射用户列表) /// private async Task BuildLoginOutputAsync(string openId, List 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 }; } /// /// 获取微信 access_token(带 Redis 缓存) /// private async Task 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(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; } /// /// 通过 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; } } /// /// 用户实体映射为输出 DTO /// 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.ToString(), Status = user.Status.ToString(), IsLastOnline = user.IsLastOnline, CreatedAt = user.CreatedAt, UpdatedAt = user.UpdatedAt, CreatedBy = user.CreatedBy, UpdatedBy = user.UpdatedBy }; } /// /// 获取微信配置 /// 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; } /// /// 获取 JWT 配置 /// 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; } }