feat(wechat-auth): 实现微信同一OpenId下多用户切换功能

1. 新增用户实体字段IsLastOnline标记上次在线用户
2. 重构微信登录逻辑,返回同一OpenId下的所有用户列表而非单用户
3. 新增SwitchUserAsync服务方法与控制器接口,支持同OpenId下切换用户
4. 添加用户实体到输出DTO的映射工具方法
5. 实现切换用户时的身份校验、Token刷新与在线状态更新
This commit is contained in:
glz
2026-06-03 18:28:45 +08:00
parent 32989e1fa0
commit 018d7e1733
5 changed files with 202 additions and 33 deletions

View File

@ -14,4 +14,12 @@ public interface IWeChatAuthService : IBaseService<Users>
/// <param name="input">登录输入(含微信 code</param>
/// <returns>登录结果(含 Token 和用户信息)</returns>
Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input);
/// <summary>
/// 切换用户(同一 OpenId 下切换身份)
/// </summary>
/// <param name="currentUserId">当前登录用户ID</param>
/// <param name="input">切换用户输入</param>
/// <returns>切换结果(含新 Token 和目标用户详情)</returns>
Task<WeChatSwitchUserOutput> SwitchUserAsync(long currentUserId, WeChatSwitchUserInput input);
}

View File

@ -24,20 +24,15 @@ public class WeChatLoginOutput
/// </summary>
public string Token { get; set; } = string.Empty;
/// <summary>
/// 用户ID
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 微信OpenId
/// </summary>
public string OpenId { get; set; } = string.Empty;
/// <summary>
/// 该 OpenId 下的用户列表
/// </summary>
public List<WxUserOutput> Users { get; set; } = [];
}
/// <summary>
@ -151,6 +146,11 @@ public class WxUserOutput
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// 是否上次在线用户
/// </summary>
public bool IsLastOnline { get; set; }
/// <summary>
/// 创建人
/// </summary>
@ -225,3 +225,30 @@ public class WxCode2SessionResponse
public string? ErrMsg { get; set; }
}
/// <summary>
/// 微信切换用户输入
/// </summary>
public class WeChatSwitchUserInput
{
/// <summary>
/// 目标用户ID
/// </summary>
public long UserId { get; set; }
}
/// <summary>
/// 微信切换用户输出
/// </summary>
public class WeChatSwitchUserOutput
{
/// <summary>
/// 新的访问令牌
/// </summary>
public string Token { get; set; } = string.Empty;
/// <summary>
/// 当前切换的用户详情
/// </summary>
public WxUserOutput User { get; set; } = new();
}

View File

@ -67,5 +67,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public string Phone { get; set; }
/// <summary>
/// Desc:是否上次在线用户
/// Default:0
/// Nullable:False
/// </summary>
public bool IsLastOnline { get; set; }
}
}

View File

@ -24,7 +24,7 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
/// 微信小程序一键登录
/// </summary>
/// <param name="input">登录输入(含微信 code</param>
/// <returns>登录结果(含 Token 和用户信息</returns>
/// <returns>登录结果(含 Token 和该 OpenId 下的用户列表</returns>
public async Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input)
{
logger.LogInformation("微信小程序登录尝试");
@ -49,14 +49,15 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
logger.LogInformation("微信 code2session 成功OpenId: {OpenId}", wxResponse.OpenId);
// 查询用户是否已存在
var user = await usersRepository.GetFirstAsync(u => u.OpenId == wxResponse.OpenId);
var isNewUser = user == null;
// 查询该 OpenId 下的所有用户
var users = await usersRepository.Context.Queryable<Users>()
.Where(u => u.OpenId == wxResponse.OpenId && !u.IsDeleted)
.ToListAsync();
if (isNewUser)
if (users.Count == 0)
{
// 首次登录,创建新用户
user = new Users
var newUser = new Users
{
Name = $"wx_{wxResponse.OpenId[^8..]}",
OpenId = wxResponse.OpenId,
@ -67,41 +68,111 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
Points = 0
};
var insertResult = await usersRepository.Insertable(user).ExecuteReturnIdentityAsync();
var insertResult = await usersRepository.Insertable(newUser).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);
newUser.Id = insertResult;
users.Add(newUser);
logger.LogInformation("微信新用户创建成功UserId: {UserId}, OpenId: {OpenId}", newUser.Id, wxResponse.OpenId);
}
else
{
// 已有用户,校验状态
if (user.Status == "Disabled")
{
logger.LogWarning("微信登录失败用户已被禁用UserId: {UserId}, OpenId: {OpenId}", user.Id, wxResponse.OpenId);
throw new BusinessException("账号已被禁用,请联系客服", 403);
logger.LogInformation("微信登录成功OpenId: {OpenId} 下存在 {Count} 个用户", wxResponse.OpenId, users.Count);
}
logger.LogInformation("微信老用户登录UserId: {UserId}, OpenId: {OpenId}", user.Id, wxResponse.OpenId);
}
// 生成 JWT Token
// 使用第一个用户生成 JWT Token
var primaryUser = users.First();
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken((long)user.Id, user.Name, jwtSettings);
var token = JwtHelper.GenerateToken((long)primaryUser.Id, primaryUser.Name, jwtSettings);
// 缓存 Token 到 Redis
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{user.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{primaryUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
// 映射用户列表
var userOutputs = users.Select(MapUserToOutput).ToList();
return new WeChatLoginOutput
{
Token = token,
UserId = (long)user.Id,
UserName = user.Name,
OpenId = wxResponse.OpenId
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)
};
}
@ -122,6 +193,31 @@ public class WeChatAuthService(BaseRepository<Users> usersRepository, IConfigura
}
}
/// <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>

View File

@ -46,4 +46,35 @@ public class WeChatAuthController : WeChatBaseController
return BaseResponse<WeChatLoginOutput>.Fail("微信登录失败,请稍后重试");
}
}
/// <summary>
/// 切换用户(同一 OpenId 下切换身份)
/// </summary>
/// <param name="input">切换用户输入含目标用户ID</param>
/// <returns>切换结果(含新 Token 和目标用户详情)</returns>
[HttpPost("switchUser")]
public async Task<BaseResponse<WeChatSwitchUserOutput>> SwitchUserAsync([FromBody] WeChatSwitchUserInput input)
{
try
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<WeChatSwitchUserOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
}
var result = await _weChatAuthService.SwitchUserAsync(userId.Value, input);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "切换用户业务异常: {Message}", ex.Message);
return BaseResponse<WeChatSwitchUserOutput>.Fail(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "切换用户系统异常");
return BaseResponse<WeChatSwitchUserOutput>.Fail("切换用户失败,请稍后重试");
}
}
}