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;
}
}