feat: SKU保存链路切换到RabbitMQ Outbox并新增独立Worker
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 51s

This commit is contained in:
2026-02-25 11:20:38 +08:00
parent aeef4ca649
commit 77caac3af9
13 changed files with 9475 additions and 18 deletions

View File

@@ -0,0 +1,44 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Application.App.Products.Messaging;
using TakeoutSaaS.Domain.Products.Enums;
using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.TenantApi.Services;
namespace TakeoutSaaS.SkuWorker.Consumers;
/// <summary>
/// SKU 保存任务消费器。
/// </summary>
public sealed class ProductSkuSaveJobRequestedConsumer(
ProductSkuSaveJobRunner jobRunner,
TakeoutAppDbContext dbContext,
ILogger<ProductSkuSaveJobRequestedConsumer> logger) : IConsumer<ProductSkuSaveJobRequestedEvent>
{
/// <inheritdoc />
public async Task Consume(ConsumeContext<ProductSkuSaveJobRequestedEvent> context)
{
var payload = context.Message;
logger.LogInformation(
"开始消费 SKU 保存任务JobId={JobId}, TenantId={TenantId}, StoreId={StoreId}, ProductId={ProductId}, RetryAttempt={RetryAttempt}",
payload.JobId,
payload.TenantId,
payload.StoreId,
payload.ProductId,
context.GetRetryAttempt());
await jobRunner.ExecuteAsync(payload.JobId);
var jobStatus = await dbContext.ProductSkuSaveJobs
.IgnoreQueryFilters()
.AsNoTracking()
.Where(item => item.Id == payload.JobId)
.Select(item => item.Status)
.SingleOrDefaultAsync(context.CancellationToken);
if (jobStatus == ProductSkuSaveJobStatus.Failed)
{
throw new InvalidOperationException($"SKU 保存任务执行失败JobId={payload.JobId}");
}
}
}

View File

@@ -0,0 +1,62 @@
using MassTransit;
using Microsoft.Extensions.Options;
using TakeoutSaaS.Infrastructure.App.Persistence;
using TakeoutSaaS.Infrastructure.Common.Extensions;
using TakeoutSaaS.Module.Messaging.Options;
using TakeoutSaaS.Module.Tenancy.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.TenantApi.Services;
using TakeoutSaaS.SkuWorker.Consumers;
using TakeoutSaaS.Application.App.Products.Messaging;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddDatabaseInfrastructure(builder.Configuration);
builder.Services.AddPostgresDbContext<TakeoutAppDbContext>(DatabaseConstants.AppDataSource);
builder.Services.AddTenantResolution(builder.Configuration);
builder.Services.AddScoped<ProductSkuSaveService>();
builder.Services.AddScoped<ProductSkuSaveJobRunner>();
builder.Services.AddOptions<RabbitMqOptions>()
.Bind(builder.Configuration.GetSection("RabbitMQ"))
.ValidateDataAnnotations()
.ValidateOnStart();
builder.Services.AddMassTransit(configurator =>
{
configurator.AddConsumer<ProductSkuSaveJobRequestedConsumer>(consumerConfigurator =>
{
consumerConfigurator.UseMessageRetry(retry =>
{
retry.Exponential(
retryLimit: 5,
minInterval: TimeSpan.FromSeconds(1),
maxInterval: TimeSpan.FromSeconds(30),
intervalDelta: TimeSpan.FromSeconds(2));
});
});
configurator.UsingRabbitMq((context, cfg) =>
{
var options = context.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
var virtualHost = string.IsNullOrWhiteSpace(options.VirtualHost) ? "/" : options.VirtualHost.Trim();
var virtualHostPath = virtualHost == "/" ? "/" : $"/{virtualHost.TrimStart('/')}";
var hostUri = new Uri($"rabbitmq://{options.Host}:{options.Port}{virtualHostPath}");
cfg.Host(hostUri, host =>
{
host.Username(options.Username);
host.Password(options.Password);
});
cfg.PrefetchCount = options.PrefetchCount;
cfg.ReceiveEndpoint(ProductSkuSaveJobRequestedEvent.QueueName, endpoint =>
{
endpoint.PrefetchCount = options.PrefetchCount;
endpoint.ConfigureConsumer<ProductSkuSaveJobRequestedConsumer>(context);
});
});
});
var app = builder.Build();
await app.RunAsync();

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MassTransit" Version="8.5.7" />
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Api\TakeoutSaaS.TenantApi\TakeoutSaaS.TenantApi.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,30 @@
{
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=localhost;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233",
"Reads": [
"Host=localhost;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
}
}
},
"Tenancy": {
"TenantIdHeaderName": "X-Tenant-Id",
"TenantCodeHeaderName": "X-Tenant-Code",
"RootDomain": "tenant.laosankeji.com"
},
"RabbitMQ": {
"Host": "localhost",
"Port": 5672,
"Username": "guest",
"Password": "guest",
"VirtualHost": "/",
"Exchange": "takeout.events",
"ExchangeType": "topic",
"PrefetchCount": 20
}
}