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; /// /// 微信小程序认证服务实现 /// public class WeChatAuthService(BaseRepository usersRepository, IConfiguration configuration, ILogger logger) : BaseRepository, IWeChatAuthService { private const string TokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token"; private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code"; /// /// 微信小程序一键登录 /// /// 登录输入(含微信 code) /// 登录结果(含 Token 和用户信息) 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); // 查询用户是否已存在 var user = await usersRepository.GetFirstAsync(u => u.OpenId == wxResponse.OpenId); var isNewUser = user == null; if (isNewUser) { // 首次登录,创建新用户 user = new Users { Name = $"wx_{wxResponse.OpenId[^8..]}", OpenId = wxResponse.OpenId, UnionId = wxResponse.UnionId, Type = "Normal", Status = "Active", GrowthPoints = 0, Points = 0 }; var insertResult = await usersRepository.Insertable(user).ExecuteReturnIdentityAsync(); if (insertResult <= 0) { logger.LogError("创建微信用户失败,OpenId: {OpenId}", wxResponse.OpenId); throw new BusinessException("创建用户失败,请稍后重试", 500); } user.Id = insertResult; logger.LogInformation("微信新用户创建成功,UserId: {UserId}, OpenId: {OpenId}", user.Id, wxResponse.OpenId); } else { // 已有用户,校验状态 if (user.Status == "Disabled") { logger.LogWarning("微信登录失败,用户已被禁用,UserId: {UserId}, OpenId: {OpenId}", user.Id, wxResponse.OpenId); throw new BusinessException("账号已被禁用,请联系客服", 403); } logger.LogInformation("微信老用户登录,UserId: {UserId}, OpenId: {OpenId}", user.Id, wxResponse.OpenId); } // 生成 JWT Token var jwtSettings = GetJwtSettings(); var token = JwtHelper.GenerateToken((long)user.Id, user.Name, jwtSettings); // 缓存 Token 到 Redis await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{user.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes)); return new WeChatLoginOutput { Token = token, UserId = (long)user.Id, UserName = user.Name, OpenId = wxResponse.OpenId }; } /// /// 调用微信 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); } } /// /// 获取微信配置 /// 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; } }