1. 新增用户实体字段IsLastOnline标记上次在线用户 2. 重构微信登录逻辑,返回同一OpenId下的所有用户列表而非单用户 3. 新增SwitchUserAsync服务方法与控制器接口,支持同OpenId下切换用户 4. 添加用户实体到输出DTO的映射工具方法 5. 实现切换用户时的身份校验、Token刷新与在线状态更新
257 lines
10 KiB
C#
257 lines
10 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 微信小程序认证服务实现
|
||
/// </summary>
|
||
public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfiguration configuration, ILogger<WeChatAuthService> logger) : BaseRepository<Users>, 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";
|
||
|
||
/// <summary>
|
||
/// 微信小程序一键登录
|
||
/// </summary>
|
||
/// <param name="input">登录输入(含微信 code)</param>
|
||
/// <returns>登录结果(含 Token 和该 OpenId 下的用户列表)</returns>
|
||
public async Task<WeChatLoginOutput> 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<Users>()
|
||
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
|
||
.ToListAsync();
|
||
|
||
if (users.Count == 0)
|
||
{
|
||
// 首次登录,创建新用户
|
||
var newUser = 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(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);
|
||
}
|
||
else
|
||
{
|
||
logger.LogInformation("微信登录成功,OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
|
||
}
|
||
|
||
// 使用第一个用户生成 JWT Token
|
||
var primaryUser = 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 = wxResponse.OpenId,
|
||
Users = userOutputs
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换用户(同一 OpenId 下切换身份)
|
||
/// </summary>
|
||
/// <param name="currentUserId">当前登录用户ID</param>
|
||
/// <param name="input">切换用户输入</param>
|
||
/// <returns>切换结果(含新 Token 和目标用户详情)</returns>
|
||
public async Task<WeChatSwitchUserOutput> 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 == "Disabled")
|
||
{
|
||
throw new BusinessException("目标账号已被禁用", 403);
|
||
}
|
||
|
||
// 更新 IsLastOnline:目标用户设为 true,同 OpenId 下其他用户设为 false
|
||
await usersRepository.Context.Updateable<Users>()
|
||
.SetColumns(u => u.IsLastOnline == false)
|
||
.Where(u => u.OpenId == openId && !u.IsDeleted)
|
||
.ExecuteCommandAsync();
|
||
|
||
await usersRepository.Context.Updateable<Users>()
|
||
.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)
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调用微信 code2session 接口
|
||
/// </summary>
|
||
private async Task<WxCode2SessionResponse?> CallCode2SessionAsync(WeChatSettings settings, string code)
|
||
{
|
||
var url = string.Format(Code2SessionUrl, settings.AppId, settings.AppSecret, code);
|
||
try
|
||
{
|
||
return await HttpHelper.GetAsync<WxCode2SessionResponse>(url);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
logger.LogError(ex, "调用微信 code2session 接口异常,URL: {Url}", url);
|
||
throw new BusinessException("微信服务请求失败,请稍后重试", 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户实体映射为输出 DTO
|
||
/// </summary>
|
||
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,
|
||
Status = user.Status,
|
||
IsLastOnline = user.IsLastOnline,
|
||
CreatedAt = user.CreatedAt,
|
||
UpdatedAt = user.UpdatedAt,
|
||
CreatedBy = user.CreatedBy,
|
||
UpdatedBy = user.UpdatedBy
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取微信配置
|
||
/// </summary>
|
||
private WeChatSettings GetWeChatSettings()
|
||
{
|
||
var settings = configuration.GetSection("WeChatSettings").Get<WeChatSettings>();
|
||
if (settings == null || string.IsNullOrWhiteSpace(settings.AppId) || string.IsNullOrWhiteSpace(settings.AppSecret))
|
||
{
|
||
logger.LogError("微信配置不完整,请检查 appsettings.json 中的 WeChatSettings 节点");
|
||
throw new BusinessException("微信配置不完整,请联系系统管理员", 500);
|
||
}
|
||
return settings;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取 JWT 配置
|
||
/// </summary>
|
||
private JwtSettings GetJwtSettings()
|
||
{
|
||
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()
|
||
?? 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;
|
||
}
|
||
}
|