Files
tiku-backend.net/Tiku.Infrastructure/Jobs/Queue/BackgroundJobService.Queue.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

97 lines
3.8 KiB
C#

using Microsoft.EntityFrameworkCore;
using Tiku.Application.Jobs;
using Tiku.Application.Security;
using Tiku.Domain.Operations;
namespace Tiku.Infrastructure.Jobs;
internal sealed partial class BackgroundJobService
{
public async Task<BackgroundJobItem> EnqueueAsync(
CreateBackgroundJobCommand command,
CancellationToken cancellationToken = default)
{
var normalizedJobType = NormalizeJobType(command.JobType);
var idempotencyKey = NormalizeIdempotencyKey(command.IdempotencyKey);
if (idempotencyKey is not null)
{
var existing = await dbContext.BackgroundJobs.AsNoTracking().SingleOrDefaultAsync(
item => item.TenantId == command.TenantId && item.JobType == normalizedJobType &&
item.IdempotencyKey == idempotencyKey,
cancellationToken);
if (existing is not null) return ToItem(existing);
}
if (!command.IsSystemJob && !(await featureAccessService.EvaluateAsync(
command.TenantId,
ResolveRequiredFeature(normalizedJobType, command.Payload),
FeatureAccessOperation.Write,
cancellationToken)).Allowed)
throw new InvalidOperationException("Tenant feature entitlement does not allow this background job.");
var quotaMetric = command.IsSystemJob ? null : ResolveQuotaMetric(normalizedJobType);
var quotaConsumed = false;
if (quotaMetric is not null)
{
var quota = (await featureAccessService.GetQuotaSummaryAsync(command.TenantId, cancellationToken))
.SingleOrDefault(value => value.MetricCode == quotaMetric);
if (quota is not null)
{
quotaConsumed = await featureAccessService.TryConsumeQuotaAsync(
command.TenantId, quotaMetric, 1, cancellationToken);
if (!quotaConsumed)
throw new FeatureAccessException("The background job quota has been exhausted.",
"feature_quota_exhausted");
}
}
var job = new BackgroundJob
{
TenantId = command.TenantId,
JobType = normalizedJobType,
IdempotencyKey = idempotencyKey,
Payload = command.Payload,
RunAfter = command.RunAfter,
MaxRetries = Math.Clamp(command.MaxRetries, 0, 20)
};
dbContext.BackgroundJobs.Add(job);
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException) when (idempotencyKey is not null)
{
dbContext.ChangeTracker.Clear();
var existing = await dbContext.BackgroundJobs.AsNoTracking().SingleOrDefaultAsync(
item => item.TenantId == command.TenantId && item.JobType == normalizedJobType &&
item.IdempotencyKey == idempotencyKey,
cancellationToken);
if (existing is not null)
{
if (quotaConsumed && quotaMetric is not null)
await featureAccessService.ReleaseQuotaAsync(command.TenantId, quotaMetric, 1,
CancellationToken.None);
return ToItem(existing);
}
throw;
}
catch
{
if (quotaConsumed && quotaMetric is not null)
await featureAccessService.ReleaseQuotaAsync(command.TenantId, quotaMetric, 1, CancellationToken.None);
throw;
}
return ToItem(job);
}
private static string? ResolveQuotaMetric(string jobType)
{
return jobType switch
{
"content_import" => SaasQuotaMetricCatalog.ImportCount,
"content_export" => SaasQuotaMetricCatalog.ExportCount,
_ => null
};
}
}