Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.WorkService/Jobs/MessageOutboxDispatchService.cs
glz cbdee5068a 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. 修复自动铺码消费逻辑,新增点阵页预占和释放机制
2026-07-10 10:44:00 +08:00

107 lines
4.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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