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:
@ -48,6 +48,8 @@ public class AutoDotCodeConsumer(
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
||||
JournalPagePrintDto? request = null;
|
||||
List<long> reservedDotDetailIds = [];
|
||||
var dotPagesCommitted = false;
|
||||
|
||||
try
|
||||
{
|
||||
@ -135,6 +137,21 @@ public class AutoDotCodeConsumer(
|
||||
}
|
||||
|
||||
var dotFileDetailPageName = dotFileDetailList.Select(x => x.PageName).OrderBy(x => x).ToArray();
|
||||
reservedDotDetailIds = dotFileDetailList.Select(x => x.Id).ToList();
|
||||
var reservedRows = await dbContext.Updateable<DotFileDetail>()
|
||||
.SetColumns(x => x.IsUse == true)
|
||||
.SetColumns(x => x.UpdatedAt == DateTime.Now)
|
||||
.Where(x => reservedDotDetailIds.Contains(x.Id) && !x.IsUse)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
if (reservedRows != reservedDotDetailIds.Count)
|
||||
{
|
||||
logger.LogError("点阵页码预占失败,DotId: {DotId}, Need: {Need}, Reserved: {Reserved}", dotId, reservedDotDetailIds.Count, reservedRows);
|
||||
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
|
||||
reservedDotDetailIds.Clear();
|
||||
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageStr = "{" + $"[{dotFileDetailList[0].PageName},{pageNumMax}]" + "}";
|
||||
var arguments = $"-sMode=Generate -sPDF=\"{uploadFilePath}\" -sLIC=\"{xmlPath}\" -pStart=1 -oPDF=\"{downloadFilePath}\" -dPageAddr=1 -dPrint={dPrint} -dDotSize=40 -dType=0 -dOutFile=0 -dControlPageNum={pageStr}";
|
||||
var printResult = await ExecutePrintToolAsync(exePath, printToolDirectory, arguments, cancellationToken);
|
||||
@ -142,6 +159,8 @@ public class AutoDotCodeConsumer(
|
||||
if (printResult.Timeout || printResult.ExitCode != 0)
|
||||
{
|
||||
logger.LogError("执行 PrintTool.exe 失败,退出码:{ExitCode},错误信息:{ErrorMessage}", printResult.ExitCode, printResult.Error);
|
||||
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
|
||||
reservedDotDetailIds.Clear();
|
||||
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
|
||||
return;
|
||||
}
|
||||
@ -154,6 +173,8 @@ public class AutoDotCodeConsumer(
|
||||
|
||||
if (!ValidatePrintOutput(printResult.Output, dotFileDetailPageName, dPrint))
|
||||
{
|
||||
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
|
||||
reservedDotDetailIds.Clear();
|
||||
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
|
||||
return;
|
||||
}
|
||||
@ -189,9 +210,12 @@ public class AutoDotCodeConsumer(
|
||||
.Where(x => x.Id == dotId)
|
||||
.ExecuteCommandAsync();
|
||||
});
|
||||
dotPagesCommitted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
|
||||
reservedDotDetailIds.Clear();
|
||||
logger.LogError("回调接口修改书籍状态失败,JournalId: {JournalId},接口返回消息:{Message}", statusModel.JournalId, callbackResponse.Message);
|
||||
}
|
||||
}
|
||||
@ -205,6 +229,11 @@ public class AutoDotCodeConsumer(
|
||||
logger.LogError(ex, "自动铺码处理失败");
|
||||
if (request != null)
|
||||
{
|
||||
if (!dotPagesCommitted && reservedDotDetailIds.Count > 0)
|
||||
{
|
||||
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, CancellationToken.None);
|
||||
}
|
||||
|
||||
await TryCallbackCodeFailAsync(request, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@ -329,6 +358,20 @@ public class AutoDotCodeConsumer(
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ReleaseReservedDotPagesAsync(ISqlSugarClient dbContext, List<long> dotDetailIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (dotDetailIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await dbContext.Updateable<DotFileDetail>()
|
||||
.SetColumns(x => x.IsUse == false)
|
||||
.SetColumns(x => x.UpdatedAt == DateTime.Now)
|
||||
.Where(x => dotDetailIds.Contains(x.Id))
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static JournalPagePrintDto BuildFailResponse(JournalPagePrintDto request, string[]? pageNo = null)
|
||||
{
|
||||
return new JournalPagePrintDto
|
||||
|
||||
@ -55,7 +55,7 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "消费者 {QueueName} 处理消息异常", queueName);
|
||||
var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, ea.Body.ToArray(), stoppingToken);
|
||||
var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, queueName, ea.Body.ToArray(), ex, stoppingToken);
|
||||
await channel.BasicNackAsync(ea.DeliveryTag, false, !dlqSent, stoppingToken);
|
||||
|
||||
try
|
||||
@ -79,7 +79,7 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, byte[] body, CancellationToken cancellationToken)
|
||||
private async Task<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, string originalQueueName, byte[] body, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -90,7 +90,14 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
|
||||
|
||||
var properties = new RabbitMQ.Client.BasicProperties
|
||||
{
|
||||
Persistent = true
|
||||
Persistent = true,
|
||||
Headers = new Dictionary<string, object?>
|
||||
{
|
||||
["x-original-queue"] = originalQueueName,
|
||||
["x-error-type"] = exception.GetType().FullName,
|
||||
["x-error-message"] = exception.Message,
|
||||
["x-failed-at"] = DateTimeOffset.UtcNow.ToString("O")
|
||||
}
|
||||
};
|
||||
await channel.BasicPublishAsync(exchange, dlqRoutingKey, false, properties, body, cancellationToken);
|
||||
logger.LogInformation("消息已发送到死信队列: {DlqName}", dlqName);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Redis;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.SDK;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using QYZH.InteractiveMagazine.PrintWorker.Consumers;
|
||||
using Serilog;
|
||||
using SqlSugar;
|
||||
@ -34,7 +35,8 @@ Log.Logger = new LoggerConfiguration()
|
||||
builder.Services.AddSerilog();
|
||||
builder.Services.AddWindowsService(options => options.ServiceName = serviceName);
|
||||
|
||||
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 3 });
|
||||
var snowflakeSettings = builder.Configuration.GetSection("SnowflakeSettings").Get<SnowflakeSettings>() ?? new SnowflakeSettings { WorkerId = 3 };
|
||||
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = snowflakeSettings.WorkerId });
|
||||
|
||||
builder.Services.AddSqlSugar(new IocConfig
|
||||
{
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "server=192.168.20.150;port=13306;database=InteractiveMagazine;user=user;password=n68792bu!y99r905;charset=utf8mb4;"
|
||||
},
|
||||
"SnowflakeSettings": {
|
||||
"WorkerId": 3
|
||||
},
|
||||
"RedisSettings": {
|
||||
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
|
||||
"Sentinels": [],
|
||||
@ -14,6 +17,10 @@
|
||||
"Password": "@ss%&*otz%d*pq2S",
|
||||
"VirtualHost": "InteractiveMagazine"
|
||||
},
|
||||
"RabbitMQRetrySettings": {
|
||||
"MaxRetryCount": 3,
|
||||
"RetryDelayMilliseconds": 30000
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
Reference in New Issue
Block a user