Tighten Gmail and export hot paths

This commit is contained in:
2026-04-11 12:10:49 +02:00
parent 33ac4b963b
commit ce26325682
2 changed files with 81 additions and 50 deletions
+60 -33
View File
@@ -137,23 +137,25 @@ public sealed class GmailController : ControllerBase
var job = await _db.JobApplications
.Where(x => x.OwnerUserId == ownerUserId)
.Include(x => x.Company)
.Include(x => x.Messages)
.FirstOrDefaultAsync(x => x.Id == jobApplicationId, cancellationToken);
if (job is null) return NotFound("Job application not found.");
var importedMessageIds = await _db.Correspondences
.Where(message => message.JobApplicationId == job.Id && !string.IsNullOrWhiteSpace(message.ExternalMessageId))
.Select(message => message.ExternalMessageId!)
.ToListAsync(cancellationToken);
var importedThreadIds = await _db.Correspondences
.Where(message => message.JobApplicationId == job.Id && !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.Select(message => message.ExternalThreadId!)
.ToListAsync(cancellationToken);
var importedMessageIdSet = importedMessageIds.ToHashSet(StringComparer.Ordinal);
var importedThreadIdSet = importedThreadIds.ToHashSet(StringComparer.Ordinal);
var queries = BuildJobQueries(job, queryOverride);
var candidateMessages = await _gmail.ListJobCandidateMessagesAsync(ownerUserId, queries, maxResultsPerQuery, cancellationToken);
var importedMessageIds = job.Messages
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalMessageId))
.Select(message => message.ExternalMessageId!)
.ToHashSet(StringComparer.Ordinal);
var importedThreadIds = job.Messages
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.Select(message => message.ExternalThreadId!)
.ToHashSet(StringComparer.Ordinal);
var rankedMessages = candidateMessages
.Select(message => _matching.ScoreMessage(job, message, importedMessageIds.Contains(message.Message.Id), importedThreadIds.Contains(message.Message.ThreadId)))
.Select(message => _matching.ScoreMessage(job, message, importedMessageIdSet.Contains(message.Message.Id), importedThreadIdSet.Contains(message.Message.ThreadId)))
.Where(result => result.Score > 0 || result.AlreadyImported)
.OrderByDescending(result => result.Score)
.ThenByDescending(result => result.Message.Date ?? DateTimeOffset.MinValue)
@@ -244,9 +246,9 @@ public sealed class GmailController : ControllerBase
return GmailNotConnectedResult();
}
var jobs = await _db.JobApplications
.AsNoTracking()
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
.Include(x => x.Company)
.Include(x => x.Messages)
.OrderByDescending(x => x.DateApplied)
.Take(100)
.ToListAsync(cancellationToken);
@@ -266,35 +268,39 @@ public sealed class GmailController : ControllerBase
var queries = querySet.Take(18).ToList();
var candidateMessages = await _gmail.ListJobCandidateMessagesAsync(ownerUserId, queries, maxResultsPerQuery, cancellationToken);
var allImportedMessageIds = jobs.SelectMany(job => job.Messages)
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalMessageId))
var allImportedMessageIds = await _db.Correspondences
.AsNoTracking()
.Where(message => message.JobApplication.OwnerUserId == ownerUserId && !string.IsNullOrWhiteSpace(message.ExternalMessageId))
.Select(message => message.ExternalMessageId!)
.ToHashSet(StringComparer.Ordinal);
var allImportedThreadIds = jobs.SelectMany(job => job.Messages)
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.ToListAsync(cancellationToken);
var allImportedThreadIds = await _db.Correspondences
.AsNoTracking()
.Where(message => message.JobApplication.OwnerUserId == ownerUserId && !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.Select(message => message.ExternalThreadId!)
.ToHashSet(StringComparer.Ordinal);
.ToListAsync(cancellationToken);
var allImportedMessageIdSet = allImportedMessageIds.ToHashSet(StringComparer.Ordinal);
var allImportedThreadIdSet = allImportedThreadIds.ToHashSet(StringComparer.Ordinal);
var reviewDecisions = await _db.GmailReviewDecisions
.AsNoTracking()
.Where(decision => decision.OwnerUserId == ownerUserId)
.ToListAsync(cancellationToken);
.ToDictionaryAsync(decision => decision.ThreadId, StringComparer.Ordinal, cancellationToken);
var groupedThreads = candidateMessages
.GroupBy(message => string.IsNullOrWhiteSpace(message.Message.ThreadId) ? message.Message.Id : message.Message.ThreadId, StringComparer.Ordinal)
.Select(group =>
{
var existingDecision = reviewDecisions.FirstOrDefault(x => x.ThreadId == group.Key);
var existingDecision = reviewDecisions.GetValueOrDefault(group.Key);
var orderedMessages = group.OrderByDescending(item => item.Message.Date ?? DateTimeOffset.MinValue).ToList();
var latestDate = orderedMessages.Max(item => item.Message.Date ?? DateTimeOffset.MinValue);
var subject = orderedMessages.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.Message.Subject))?.Message.Subject ?? "(no subject)";
var matchedQueries = orderedMessages.SelectMany(item => item.MatchedQueries).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
var hasImportedMessages = orderedMessages.Any(item => allImportedMessageIds.Contains(item.Message.Id) || allImportedThreadIds.Contains(item.Message.ThreadId));
var hasImportedMessages = orderedMessages.Any(item => allImportedMessageIdSet.Contains(item.Message.Id) || allImportedThreadIdSet.Contains(item.Message.ThreadId));
var jobCandidates = jobs
.Select(job =>
{
var best = orderedMessages
.Select(item => _matching.ScoreMessage(job, item, allImportedMessageIds.Contains(item.Message.Id), allImportedThreadIds.Contains(item.Message.ThreadId)))
.Select(item => _matching.ScoreMessage(job, item, allImportedMessageIdSet.Contains(item.Message.Id), allImportedThreadIdSet.Contains(item.Message.ThreadId)))
.OrderByDescending(score => score.Score)
.First();
return new GmailReviewJobCandidateDto(
@@ -335,7 +341,7 @@ public sealed class GmailController : ControllerBase
item.Message.Snippet,
item.MatchedQueries.Count * 4,
item.MatchedQueries.Count >= 2 ? "medium" : "low",
allImportedMessageIds.Contains(item.Message.Id),
allImportedMessageIdSet.Contains(item.Message.Id),
item.MatchedQueries,
Array.Empty<GmailJobMatchReasonDto>()))
.ToList();
@@ -435,9 +441,9 @@ public sealed class GmailController : ControllerBase
return GmailNotConnectedResult();
}
var jobs = await _db.JobApplications
.AsNoTracking()
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
.Include(x => x.Company)
.Include(x => x.Messages)
.OrderByDescending(x => x.DateApplied)
.Take(100)
.ToListAsync(cancellationToken);
@@ -462,17 +468,19 @@ public sealed class GmailController : ControllerBase
var queries = querySet.Take(24).ToList();
var candidateMessages = await _gmail.ListJobCandidateMessagesAsync(ownerUserId, queries, maxResultsPerQuery, cancellationToken);
var allImportedMessageIds = jobs.SelectMany(jobItem => jobItem.Messages)
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalMessageId))
var allImportedMessageIds = await _db.Correspondences
.Where(message => message.JobApplication.OwnerUserId == ownerUserId && !string.IsNullOrWhiteSpace(message.ExternalMessageId))
.Select(message => message.ExternalMessageId!)
.ToHashSet(StringComparer.Ordinal);
var allImportedThreadIds = jobs.SelectMany(jobItem => jobItem.Messages)
.Where(message => !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.ToListAsync(cancellationToken);
var allImportedThreadIds = await _db.Correspondences
.Where(message => message.JobApplication.OwnerUserId == ownerUserId && !string.IsNullOrWhiteSpace(message.ExternalThreadId))
.Select(message => message.ExternalThreadId!)
.ToHashSet(StringComparer.Ordinal);
.ToListAsync(cancellationToken);
var allImportedMessageIdSet = allImportedMessageIds.ToHashSet(StringComparer.Ordinal);
var allImportedThreadIdSet = allImportedThreadIds.ToHashSet(StringComparer.Ordinal);
var reviewDecisions = await _db.GmailReviewDecisions
.Where(decision => decision.OwnerUserId == ownerUserId)
.ToListAsync(cancellationToken);
.ToDictionaryAsync(decision => decision.ThreadId, StringComparer.Ordinal, cancellationToken);
var groupedThreads = candidateMessages
.GroupBy(message => string.IsNullOrWhiteSpace(message.Message.ThreadId) ? message.Message.Id : message.Message.ThreadId, StringComparer.Ordinal)
@@ -488,7 +496,7 @@ public sealed class GmailController : ControllerBase
foreach (var threadGroup in groupedThreads)
{
var threadId = threadGroup.Key;
var existingDecision = reviewDecisions.FirstOrDefault(x => x.ThreadId == threadId);
var existingDecision = reviewDecisions.GetValueOrDefault(threadId);
if (string.Equals(existingDecision?.Decision, "rejected", StringComparison.OrdinalIgnoreCase))
{
unmatchedCount++;
@@ -500,7 +508,7 @@ public sealed class GmailController : ControllerBase
.Select(jobItem =>
{
var best = orderedMessages
.Select(item => _matching.ScoreMessage(jobItem, item, allImportedMessageIds.Contains(item.Message.Id), allImportedThreadIds.Contains(item.Message.ThreadId)))
.Select(item => _matching.ScoreMessage(jobItem, item, allImportedMessageIdSet.Contains(item.Message.Id), allImportedThreadIdSet.Contains(item.Message.ThreadId)))
.OrderByDescending(score => score.Score)
.First();
return new { Job = jobItem, Best = best };
@@ -528,7 +536,7 @@ public sealed class GmailController : ControllerBase
}
await ImportSingleMessageAsync(ownerUserId, top.Job, messageId, cancellationToken);
allImportedMessageIds.Add(messageId);
allImportedMessageIdSet.Add(messageId);
importedMessages++;
}
@@ -1022,6 +1030,25 @@ public sealed class GmailController : ControllerBase
|| sample.Contains("rejection", StringComparison.OrdinalIgnoreCase);
}
private void UpsertReviewDecision(IDictionary<string, GmailReviewDecision> decisions, string ownerUserId, string threadId, string decision, int? jobApplicationId, string? note)
{
if (!decisions.TryGetValue(threadId, out var existing))
{
existing = new GmailReviewDecision
{
OwnerUserId = ownerUserId,
ThreadId = threadId,
};
decisions[threadId] = existing;
_db.GmailReviewDecisions.Add(existing);
}
existing.Decision = decision;
existing.JobApplicationId = jobApplicationId;
if (!string.IsNullOrWhiteSpace(note)) existing.Note = note.Trim();
existing.UpdatedAt = DateTimeOffset.UtcNow;
}
private void UpsertReviewDecision(List<GmailReviewDecision> decisions, string ownerUserId, string threadId, string decision, int? jobApplicationId, string? note)
{
var existing = decisions.FirstOrDefault(x => x.ThreadId == threadId);