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:
@ -0,0 +1,106 @@
|
||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using SqlSugar;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.WorkService.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 消息Outbox派发服务。
|
||||
/// </summary>
|
||||
public class MessageOutboxDispatchService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IRabbitMQService rabbitMQService,
|
||||
IConfiguration configuration,
|
||||
ILogger<MessageOutboxDispatchService> logger) : BackgroundService
|
||||
{
|
||||
private const int BatchSize = 50;
|
||||
private readonly RabbitMQRetrySettings retrySettings = configuration.GetSection("RabbitMQRetrySettings").Get<RabbitMQRetrySettings>() ?? new RabbitMQRetrySettings();
|
||||
|
||||
/// <summary>
|
||||
/// 执行Outbox派发循环。
|
||||
/// </summary>
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DispatchPendingMessagesAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Outbox消息派发循环异常");
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DispatchPendingMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
||||
var now = DateTime.Now;
|
||||
var messages = await db.Queryable<MessageOutbox>()
|
||||
.Where(x => !x.IsDeleted
|
||||
&& (x.Status == (int)MessageOutboxStatusEnum.Pending || x.Status == (int)MessageOutboxStatusEnum.Failed)
|
||||
&& (x.NextRetryAt == null || x.NextRetryAt <= now))
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.Take(BatchSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
await DispatchMessageAsync(db, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DispatchMessageAsync(ISqlSugarClient db, MessageOutbox message, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var payload = JsonDocument.Parse(message.Payload);
|
||||
var sent = await rabbitMQService.SendAsync(new RabbitMQSendParam
|
||||
{
|
||||
Exchange = message.Exchange,
|
||||
Queue = message.Queue,
|
||||
RoutingKey = message.RoutingKey,
|
||||
Data = payload.RootElement.Clone()
|
||||
}, cancellationToken);
|
||||
|
||||
if (!sent)
|
||||
{
|
||||
throw new InvalidOperationException("RabbitMQ SendAsync returned false");
|
||||
}
|
||||
|
||||
await db.Updateable<MessageOutbox>()
|
||||
.SetColumns(x => x.Status == (int)MessageOutboxStatusEnum.Sent)
|
||||
.SetColumns(x => x.SentAt == DateTime.Now)
|
||||
.SetColumns(x => x.UpdatedAt == DateTime.Now)
|
||||
.Where(x => x.Id == message.Id && x.Status != (int)MessageOutboxStatusEnum.Sent)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var retryCount = message.RetryCount + 1;
|
||||
var abandoned = retryCount >= retrySettings.MaxRetryCount;
|
||||
await db.Updateable<MessageOutbox>()
|
||||
.SetColumns(x => x.Status == (int)(abandoned ? MessageOutboxStatusEnum.Abandoned : MessageOutboxStatusEnum.Failed))
|
||||
.SetColumns(x => x.RetryCount == retryCount)
|
||||
.SetColumns(x => x.NextRetryAt == (abandoned ? null : DateTime.Now.AddMilliseconds(retrySettings.RetryDelayMilliseconds)))
|
||||
.SetColumns(x => x.LastError == ex.Message)
|
||||
.SetColumns(x => x.UpdatedAt == DateTime.Now)
|
||||
.Where(x => x.Id == message.Id)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
|
||||
logger.LogError(ex, "Outbox消息派发失败,MessageId: {MessageId}, RetryCount: {RetryCount}", message.Id, retryCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user