feat: 新增消息Outbox机制、雪花ID配置优化及多项功能完善
1. 新增数据库唯一约束和Message_Outbox表脚本 2. 新增雪花ID、Hangfire存储、MQ重试等配置实体 3. 重构各项目雪花ID生成逻辑,改为从配置读取WorkerId 4. 优化积分服务分页查询、用户背包更新逻辑 5. 新增JWT令牌Redis过期刷新逻辑 6. 完善RabbitMQ死信队列消息头信息 7. 新增可靠MQ消息发布服务和Outbox派发后台服务 8. 替换原有RabbitMQ直接发送为Outbox可靠发布 9. 优化签到服务逻辑,新增重复签到校验和补签卡扣减逻辑 10. 修复自动铺码消费逻辑,新增点阵页预占和释放机制
This commit is contained in:
@ -106,6 +106,7 @@ public static class DependencyInjectionExtensions
|
||||
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
|
||||
if (adminToken == currentToken)
|
||||
{
|
||||
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildAdminTokenKey(userId), currentToken, jwtSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -116,6 +117,7 @@ public static class DependencyInjectionExtensions
|
||||
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
|
||||
if (wechatToken == currentToken)
|
||||
{
|
||||
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId), currentToken, jwtSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,6 +126,14 @@ public static class DependencyInjectionExtensions
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task RefreshRedisTokenExpiryAsync(string tokenKey, string currentToken, JwtSettings jwtSettings)
|
||||
{
|
||||
if (jwtSettings.ExpiryMinutes > 0)
|
||||
{
|
||||
await RedisHelper.SetAsync(tokenKey, currentToken, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetBearerToken(TokenValidatedContext context)
|
||||
{
|
||||
var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
|
||||
|
||||
@ -1,99 +1,21 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the Redis session TTL for the current JWT when it is still the active token.
|
||||
/// 保留兼容的JWT自动刷新中间件,实际刷新在认证成功后执行。
|
||||
/// </summary>
|
||||
public class JwtAutoRefreshMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
|
||||
public JwtAutoRefreshMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
|
||||
{
|
||||
var token = authHeader.Substring("Bearer ".Length).Trim();
|
||||
await TryRefreshRedisTokenExpiryAsync(token);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private async Task TryRefreshRedisTokenExpiryAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
if (tokenHandler.ReadToken(token) is not JwtSecurityToken jwtToken)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (jwtToken.ValidTo <= DateTime.UtcNow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userId = GetClaimValue(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
|
||||
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var wxUserId = GetClaimValue(jwtToken, JwtHelper.WxUserIdClaimType);
|
||||
await RefreshRedisTokenExpiryAsync(wxUserId, userId, token, jwtSettings.ExpiryMinutes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore refresh failures. Authentication middleware will validate the request later.
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RefreshRedisTokenExpiryAsync(string? wxUserId, string userId, string currentToken, int expiryMinutes)
|
||||
{
|
||||
var adminTokenKey = JwtHelper.BuildAdminTokenKey(userId);
|
||||
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
|
||||
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
|
||||
{
|
||||
await RedisHelper.SetAsync(adminTokenKey, adminToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(wxUserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var wechatTokenKey = JwtHelper.BuildWeChatTokenKey(wxUserId, userId);
|
||||
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
|
||||
if (wechatToken == currentToken)
|
||||
{
|
||||
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetClaimValue(JwtSecurityToken jwtToken, params string[] claimTypes)
|
||||
{
|
||||
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type))?.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
@ -11,14 +12,16 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
private readonly IRabbitMQConnection _connection;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<RabbitMQService> _logger;
|
||||
private readonly JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration)
|
||||
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration, ILogger<RabbitMQService> logger)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@ -55,12 +58,14 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Console.WriteLine($"Operation was canceled: {ex.Message}");
|
||||
_logger.LogWarning(ex, "RabbitMQ消息发送已取消,Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
|
||||
param.Exchange, param.Queue, param.RoutingKey);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"An error occurred: {ex.Message}");
|
||||
_logger.LogError(ex, "RabbitMQ消息发送失败,Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
|
||||
param.Exchange, param.Queue, param.RoutingKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -115,7 +120,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"An error occurred: {ex.Message}");
|
||||
_logger.LogError(ex, "RabbitMQ批量消息发送失败");
|
||||
// 回滚事务
|
||||
try { await channel?.TxRollbackAsync(); } catch { /* 忽略回滚异常 */ }
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user