Files
tiku-backend.net/Tiku.Infrastructure/Jobs/Operations/BackgroundJobService.Operations.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

146 lines
5.7 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Jobs;
using Tiku.Domain.Operations;
namespace Tiku.Infrastructure.Jobs;
internal sealed partial class BackgroundJobService
{
public async Task<IReadOnlyCollection<BackgroundJobItem>> ListAsync(
Guid tenantId,
string? jobType = null,
int limit = 50,
CancellationToken cancellationToken = default)
{
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking()
.Where(job => job.TenantId == tenantId);
if (!string.IsNullOrWhiteSpace(jobType))
{
var normalized = NormalizeJobType(jobType);
query = query.Where(job => job.JobType == normalized);
}
var jobs = await query
.OrderByDescending(job => job.CreatedAt)
.Take(Math.Clamp(limit, 1, 200))
.ToArrayAsync(cancellationToken);
return jobs.Select(ToItem).ToArray();
}
public async Task<BackgroundJobItem?> GetAsync(
Guid jobId,
Guid? tenantId,
CancellationToken cancellationToken = default)
{
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking().Where(item => item.Id == jobId);
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
var job = await query.SingleOrDefaultAsync(cancellationToken);
return job is null ? null : ToItem(job);
}
public async Task<IReadOnlyCollection<BackgroundJobItem>> ListPlatformAsync(
Guid? tenantId = null,
string? jobType = null,
BackgroundJobStatus? status = null,
int limit = 100,
CancellationToken cancellationToken = default)
{
var query = jobsOperationsPersistence.BackgroundJobs.AsNoTracking().AsQueryable();
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
if (!string.IsNullOrWhiteSpace(jobType))
{
var normalized = NormalizeJobType(jobType);
query = query.Where(item => item.JobType == normalized);
}
if (status.HasValue) query = query.Where(item => item.Status == status.Value);
return (await query.OrderByDescending(item => item.CreatedAt)
.Take(Math.Clamp(limit, 1, 500))
.ToArrayAsync(cancellationToken))
.Select(ToItem)
.ToArray();
}
public async Task<BackgroundJobItem> RequestCancellationAsync(
Guid jobId,
Guid? tenantId,
Guid actorUserId,
string reason,
CancellationToken cancellationToken = default)
{
unitOfWork.ChangeTracker.Clear();
if (string.IsNullOrWhiteSpace(reason))
throw new BackgroundJobException("background_job_cancel_reason_required",
"Cancellation reason is required.");
var job = await FindMutableAsync(jobId, tenantId, cancellationToken);
if (job.Status is BackgroundJobStatus.Succeeded or BackgroundJobStatus.Failed or BackgroundJobStatus.Cancelled)
throw new BackgroundJobException("background_job_not_cancellable",
"Only pending or processing jobs can be cancelled.");
var now = DateTimeOffset.UtcNow;
job.CancellationRequestedAt = now;
job.CancellationRequestedBy = actorUserId;
job.CancellationReason = reason.Trim();
if (job.Status == BackgroundJobStatus.Pending)
{
job.Status = BackgroundJobStatus.Cancelled;
job.CompletedAt = now;
}
AddMutationAudit(job, actorUserId, "background_job.cancel_requested");
await unitOfWork.SaveChangesAsync(cancellationToken);
return ToItem(job);
}
public async Task<BackgroundJobItem> RetryAsync(
Guid jobId,
Guid? tenantId,
Guid actorUserId,
CancellationToken cancellationToken = default)
{
unitOfWork.ChangeTracker.Clear();
var job = await FindMutableAsync(jobId, tenantId, cancellationToken);
if (job.Status is not (BackgroundJobStatus.Failed or BackgroundJobStatus.Cancelled))
throw new BackgroundJobException("background_job_not_retryable",
"Only failed or cancelled jobs can be retried.");
job.Status = BackgroundJobStatus.Pending;
job.RunAfter = DateTimeOffset.UtcNow;
job.StartedAt = null;
job.CompletedAt = null;
job.LockedBy = null;
job.LockExpiresAt = null;
job.LastError = null;
job.CancellationRequestedAt = null;
job.CancellationRequestedBy = null;
job.CancellationReason = null;
AddMutationAudit(job, actorUserId, "background_job.retry_requested");
await unitOfWork.SaveChangesAsync(cancellationToken);
return ToItem(job);
}
private async Task<BackgroundJob> FindMutableAsync(
Guid jobId,
Guid? tenantId,
CancellationToken cancellationToken)
{
var query = jobsOperationsPersistence.BackgroundJobs.Where(item => item.Id == jobId);
if (tenantId.HasValue) query = query.Where(item => item.TenantId == tenantId.Value);
return await query.SingleOrDefaultAsync(cancellationToken) ??
throw new BackgroundJobException("background_job_not_found", "Background job was not found.");
}
private void AddMutationAudit(BackgroundJob job, Guid actorUserId, string action)
{
jobsOperationsPersistence.AuditLogs.Add(new AuditLog
{
TenantId = job.TenantId,
ActorUserId = actorUserId,
Action = action,
TargetType = "background_job",
TargetId = job.Id.ToString(),
Details = JsonSerializer.SerializeToElement(new { job.JobType, job.Status })
});
}
}