Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71d5ac8e06 | |||
| c2082d6729 | |||
| 6f1d8992ab | |||
| 2d9ffc9c2b | |||
| 9fbad722fc | |||
| 473c36d65f | |||
| 292d19d5ed | |||
| d56729de42 | |||
| 79a3dec679 | |||
| 02d2b1e510 | |||
| 3c3451b198 | |||
| a83f6f705f | |||
| b68cf942a8 | |||
| 61805e2fb5 | |||
| 4de6f1db45 | |||
| 2e9069cbdb |
@@ -10,4 +10,6 @@ public sealed class JobMatchRequest
|
||||
public string? CaptchaToken { get; set; }
|
||||
/// <summary>ISO 639-1 language code for the match result (e.g. "en", "ro"). Defaults to "en".</summary>
|
||||
public string? Language { get; set; }
|
||||
/// <summary>Client IP address — set by the api layer from the HTTP context before forwarding. Not supplied by the browser.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ public interface IJobSearchApi
|
||||
Task<CreateJobSearchTokenResponse> CreateTokenAsync([Body] CreateJobSearchTokenRequest request, CancellationToken ct);
|
||||
|
||||
[Post("/api/cv/job-search/token/{tokenId}/start")]
|
||||
Task<StartJobSearchResponse> StartSearchAsync(string tokenId, CancellationToken ct);
|
||||
Task<StartJobSearchResponse> StartSearchAsync(string tokenId, [Body] StartJobSearchRequest request, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ public sealed class CvMatcherController : ControllerBase
|
||||
return BadRequest(new ErrorResponse { Error = "Captcha verification failed.", Code = "captcha_verification_failed" });
|
||||
}
|
||||
|
||||
request.ClientIpAddress = userIp;
|
||||
_logger.LogInformation("Proxying job match request to cv-matcher-api. CvDocumentId={CvDocumentId}, HasJobUrl={HasJobUrl}, HasJobDescription={HasJobDescription}",
|
||||
request.CvDocumentId,
|
||||
!string.IsNullOrWhiteSpace(request.JobUrl),
|
||||
@@ -181,7 +182,7 @@ public sealed class CvMatcherController : ControllerBase
|
||||
try
|
||||
{
|
||||
var tokenResp = await _jobSearchApi.CreateTokenAsync(
|
||||
new CreateJobSearchTokenRequest { CvDocumentId = request.CvDocumentId, Email = request.Email, Language = language, Keywords = res.Keywords, Location = res.Location },
|
||||
new CreateJobSearchTokenRequest { CvDocumentId = request.CvDocumentId, Email = request.Email, Language = language, Keywords = res.Keywords, Location = res.Location, ClientIpAddress = userIp },
|
||||
ct);
|
||||
if (!string.IsNullOrWhiteSpace(tokenResp.TokenId))
|
||||
{
|
||||
@@ -244,7 +245,8 @@ public sealed class CvMatcherController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _jobSearchApi.StartSearchAsync(t, ct);
|
||||
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
var result = await _jobSearchApi.StartSearchAsync(t, new StartJobSearchRequest { ClientIpAddress = userIp }, ct);
|
||||
var lang = "en";
|
||||
var (title, message) = result.Status switch
|
||||
{
|
||||
|
||||
@@ -7,4 +7,6 @@ public sealed class CreateJobSearchTokenRequest
|
||||
public string Language { get; set; } = "en";
|
||||
public List<string> Keywords { get; set; } = [];
|
||||
public string? Location { get; set; }
|
||||
/// <summary>Client IP address forwarded by the api layer at CV match time. Null when not available.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,5 +9,7 @@
|
||||
public string? Email { get; set; }
|
||||
/// <summary>ISO 639-1 language code for the match result (e.g. "en", "ro"). Defaults to "en".</summary>
|
||||
public string? Language { get; set; }
|
||||
/// <summary>Client IP address forwarded by the api layer. Null when called from a background job.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace CvMatcher.Models.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// Request body sent by <c>api</c> when activating a one-time job-search link.
|
||||
/// Carries the caller's IP address so it can be persisted on the session for auditing.
|
||||
/// </summary>
|
||||
public sealed class StartJobSearchRequest
|
||||
{
|
||||
/// <summary>Client IP address forwarded by the api layer. Null when not available.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
@@ -21,8 +21,6 @@ public sealed class JobProviderConfig
|
||||
public string JobLinkContains { get; set; } = string.Empty;
|
||||
public List<string> InitialKeywords { get; set; } = [];
|
||||
public int MaxResults { get; set; } = 20;
|
||||
/// <summary>When true the scraper uses a headless Chromium browser to render JS-heavy pages.</summary>
|
||||
public bool UseHeadlessBrowser { get; set; }
|
||||
/// <summary>
|
||||
/// When false, the Stage 2 anchor-text keyword filter is skipped.
|
||||
/// Set to false for providers whose search URL already filters by relevance server-side.
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class JobSearchController : ControllerBase
|
||||
if (string.IsNullOrWhiteSpace(request.CvDocumentId) || string.IsNullOrWhiteSpace(request.Email))
|
||||
return BadRequest(new ErrorResponse { Error = "CvDocumentId and Email are required.", Code = "invalid_request" });
|
||||
|
||||
var tokenId = await _tokenService.CreateTokenAsync(request.CvDocumentId, request.Email, request.Language, request.Keywords, request.Location, ct);
|
||||
var tokenId = await _tokenService.CreateTokenAsync(request.CvDocumentId, request.Email, request.Language, request.Keywords, request.Location, request.ClientIpAddress, ct);
|
||||
return Ok(new CreateJobSearchTokenResponse { TokenId = tokenId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -81,11 +81,11 @@ public sealed class JobSearchController : ControllerBase
|
||||
[SwaggerResponse(StatusCodes.Status500InternalServerError, "Session creation failed", typeof(ErrorResponse))]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<StartJobSearchResponse>> Start(string tokenId, CancellationToken ct)
|
||||
public async Task<ActionResult<StartJobSearchResponse>> Start(string tokenId, [FromBody] StartJobSearchRequest? request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await _tokenService.TriggerStartAsync(tokenId, ct);
|
||||
var status = await _tokenService.TriggerStartAsync(tokenId, request?.ClientIpAddress, ct);
|
||||
return Ok(new StartJobSearchResponse { Status = status });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -19,16 +19,17 @@ public interface IJobTokenService
|
||||
/// The generated token ID to embed in the one-click job search link,
|
||||
/// or <c>null</c> when no job providers are currently enabled (link should be suppressed).
|
||||
/// </returns>
|
||||
Task<string?> CreateTokenAsync(string cvDocumentId, string email, string language, IReadOnlyList<string> keywords, string? location, CancellationToken ct);
|
||||
Task<string?> CreateTokenAsync(string cvDocumentId, string email, string language, IReadOnlyList<string> keywords, string? location, string? clientIpAddress, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the token and, if valid, marks it as used and creates a <c>Pending</c> job search session.
|
||||
/// </summary>
|
||||
/// <param name="tokenId">The token ID from the one-click link.</param>
|
||||
/// <param name="clientIpAddress">Client IP address forwarded by the api layer. Null when not available.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// One of the <c>StartJobSearchStatus</c> string constants:
|
||||
/// <c>Started</c>, <c>AlreadyUsed</c>, <c>Expired</c>, or <c>NotFound</c>.
|
||||
/// </returns>
|
||||
Task<string> TriggerStartAsync(string tokenId, CancellationToken ct);
|
||||
Task<string> TriggerStartAsync(string tokenId, string? clientIpAddress, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
{
|
||||
var job = await _rag.GetDocumentAsync(result.DocumentId, ct);
|
||||
if (job is null) continue;
|
||||
jobs.Add(await ScorePairAsync(cv, job, result.MatchedChunks.Select(x => x.Text).ToArray(), request.Email, NormalizeLanguage(null), ct));
|
||||
jobs.Add(await ScorePairAsync(cv, job, result.MatchedChunks.Select(x => x.Text).ToArray(), request.Email, null, NormalizeLanguage(null), ct));
|
||||
}
|
||||
|
||||
return new FindJobsResponse { CvDocumentId = request.CvDocumentId, Jobs = jobs };
|
||||
@@ -107,7 +107,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
.FirstOrDefault(x => x.DocumentId == job.DocumentId)?
|
||||
.MatchedChunks.Select(x => x.Text).ToArray() ?? [];
|
||||
|
||||
return await ScorePairAsync(cv, jobDocument, matchedChunks, request.Email, NormalizeLanguage(request.Language), ct);
|
||||
return await ScorePairAsync(cv, jobDocument, matchedChunks, request.Email, request.ClientIpAddress, NormalizeLanguage(request.Language), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,7 +115,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
/// Returns a cached result immediately when the same (CV, job, language) triple has been scored before.
|
||||
/// When no evidence chunks are available from the vector search, falls back to the raw job text.
|
||||
/// </summary>
|
||||
private async Task<JobMatchResponse> ScorePairAsync(RagDocumentDetails cv, RagDocumentDetails job, IReadOnlyList<string> evidenceChunks, string? email, string language, CancellationToken ct)
|
||||
private async Task<JobMatchResponse> ScorePairAsync(RagDocumentDetails cv, RagDocumentDetails job, IReadOnlyList<string> evidenceChunks, string? email, string? clientIpAddress, string language, CancellationToken ct)
|
||||
{
|
||||
var cached = await _repository.GetMatchAsync(cv.Id, job.Id, language, ct);
|
||||
if (cached is not null) return cached;
|
||||
@@ -145,7 +145,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
result.JobDocumentId = job.Id;
|
||||
result.JobUrl = job.SourceUrl;
|
||||
result.Cached = false;
|
||||
await _repository.SaveMatchAsync(cv.Id, job.Id, language, result, ct);
|
||||
await _repository.SaveMatchAsync(cv.Id, job.Id, language, result, email, clientIpAddress, ct);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> CreateTokenAsync(string cvDocumentId, string email, string language, IReadOnlyList<string> keywords, string? location, CancellationToken ct)
|
||||
public async Task<string?> CreateTokenAsync(string cvDocumentId, string email, string language, IReadOnlyList<string> keywords, string? location, string? clientIpAddress, CancellationToken ct)
|
||||
{
|
||||
var hasEnabledProviders = await _db.JobProviders.AnyAsync(p => p.Enabled, ct);
|
||||
if (!hasEnabledProviders)
|
||||
@@ -51,6 +51,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
Language = language,
|
||||
Keywords = string.Join(",", keywords),
|
||||
Location = location,
|
||||
ClientIpAddress = clientIpAddress,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(_settings.TokenExpiryDays),
|
||||
Used = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
@@ -63,7 +64,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> TriggerStartAsync(string tokenId, CancellationToken ct)
|
||||
public async Task<string> TriggerStartAsync(string tokenId, string? clientIpAddress, CancellationToken ct)
|
||||
{
|
||||
var token = await _db.JobSearchTokens.FirstOrDefaultAsync(x => x.Id == tokenId, ct);
|
||||
if (token is null) return StartJobSearchStatus.NotFound;
|
||||
@@ -94,6 +95,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
Status = JobSearchStatus.Pending,
|
||||
Keywords = keywords,
|
||||
Location = token.Location,
|
||||
ClientIpAddress = clientIpAddress,
|
||||
ProviderConfigJson = providerConfigJson,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
@@ -128,7 +130,6 @@ public sealed class JobTokenService : IJobTokenService
|
||||
JobLinkContains = entity.JobLinkContains,
|
||||
InitialKeywords = keywords,
|
||||
MaxResults = entity.MaxResults,
|
||||
UseHeadlessBrowser = entity.UseHeadlessBrowser,
|
||||
RequireKeywordInAnchor = entity.RequireKeywordInAnchor
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ public sealed class CvMatcherDbContext : DbContext
|
||||
entity.Property(x => x.JobDocumentId).HasMaxLength(64).IsRequired();
|
||||
entity.Property(x => x.ResultJson).IsRequired();
|
||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
entity.Property(x => x.Email).HasMaxLength(256);
|
||||
entity.Property(x => x.ClientIpAddress).HasMaxLength(45);
|
||||
entity.HasIndex(x => new { x.CvDocumentId, x.JobDocumentId, x.Language }).IsUnique();
|
||||
});
|
||||
|
||||
|
||||
@@ -9,4 +9,6 @@ public sealed class CvMatchResultEntity : BaseEntity
|
||||
public string Language { get; set; } = "en";
|
||||
public string ResultJson { get; set; } = string.Empty;
|
||||
public int Score { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CvMatcher.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvMatcher.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CvMatcherDbContext))]
|
||||
[Migration("20260608155310_AddEmailAndIpToResults")]
|
||||
partial class AddEmailAndIpToResults
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("cvMatcher")
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CvMatcher.Data.Entities.AiPromptEntity", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Key", "Language");
|
||||
|
||||
b.ToTable("AiPrompts", "cvMatcher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CvDocumentId", "JobDocumentId", "Language")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Results", "cvMatcher");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
|
||||
{
|
||||
b.Property<string>("CacheKey")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("ResponseText")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<decimal>("Temperature")
|
||||
.HasColumnType("decimal(4,2)");
|
||||
|
||||
b.HasKey("CacheKey");
|
||||
|
||||
b.ToTable("ChatCache", "cvMatcher");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using CvMatcher.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvMatcher.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailAndIpToResults : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "Results",
|
||||
type: "nvarchar(45)",
|
||||
maxLength: 45,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Email",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "Results",
|
||||
type: "nvarchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "Results");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Email",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "Results");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,10 @@ namespace CvMatcher.Data.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
@@ -70,6 +74,10 @@ namespace CvMatcher.Data.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
|
||||
@@ -6,7 +6,7 @@ public interface IMatcherRepository
|
||||
{
|
||||
Task InitializeAsync(CancellationToken ct);
|
||||
Task<JobMatchResponse?> GetMatchAsync(string cvDocumentId, string jobDocumentId, string language, CancellationToken ct);
|
||||
Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, CancellationToken ct);
|
||||
Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, string? email, string? clientIpAddress, CancellationToken ct);
|
||||
Task<string?> GetChatCompletionAsync(string cacheKey, CancellationToken ct);
|
||||
Task SaveChatCompletionAsync(string cacheKey, string model, decimal temperature, string responseText, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class EfMatcherRepository : IMatcherRepository
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, CancellationToken ct)
|
||||
public async Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, string? email, string? clientIpAddress, CancellationToken ct)
|
||||
{
|
||||
var exists = await _db.CvMatchResults.AnyAsync(
|
||||
x => x.CvDocumentId == cvDocumentId && x.JobDocumentId == jobDocumentId && x.Language == language,
|
||||
@@ -58,6 +58,8 @@ public sealed class EfMatcherRepository : IMatcherRepository
|
||||
Language = language,
|
||||
ResultJson = JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
|
||||
Score = response.Score,
|
||||
Email = email,
|
||||
ClientIpAddress = clientIpAddress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ public sealed class CvSearchDbContext : DbContext
|
||||
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
|
||||
entity.Property(x => x.Keywords).HasMaxLength(1000).HasDefaultValue(string.Empty);
|
||||
entity.Property(x => x.Used).HasDefaultValue(false);
|
||||
entity.Property(x => x.ClientIpAddress).HasMaxLength(45);
|
||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
});
|
||||
|
||||
@@ -51,6 +52,7 @@ public sealed class CvSearchDbContext : DbContext
|
||||
entity.Property(x => x.Keywords).HasMaxLength(1000);
|
||||
entity.Property(x => x.ProviderConfigJson).IsRequired(false);
|
||||
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
|
||||
entity.Property(x => x.ClientIpAddress).HasMaxLength(45);
|
||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
entity.HasIndex(x => x.Status);
|
||||
});
|
||||
@@ -64,6 +66,8 @@ public sealed class CvSearchDbContext : DbContext
|
||||
entity.Property(x => x.ProviderName).HasMaxLength(128);
|
||||
entity.Property(x => x.JobUrl).HasMaxLength(2048);
|
||||
entity.Property(x => x.JobTitle).HasMaxLength(512);
|
||||
entity.Property(x => x.Email).HasMaxLength(256);
|
||||
entity.Property(x => x.ClientIpAddress).HasMaxLength(45);
|
||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
entity.HasIndex(x => x.SessionId);
|
||||
});
|
||||
@@ -79,7 +83,6 @@ public sealed class CvSearchDbContext : DbContext
|
||||
entity.Property(x => x.InitialKeywordsJson).HasMaxLength(2000).HasDefaultValue("[]").IsRequired();
|
||||
entity.Property(x => x.MaxResults).HasDefaultValue(20);
|
||||
entity.Property(x => x.DisplayOrder).HasDefaultValue(0);
|
||||
entity.Property(x => x.UseHeadlessBrowser).HasDefaultValue(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,6 @@ public sealed class JobProviderEntity
|
||||
/// <summary>Controls display ordering in future admin UIs.</summary>
|
||||
public int DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>When true, the scraper renders the page with headless Chromium instead of a plain HTTP GET.</summary>
|
||||
public bool UseHeadlessBrowser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When false, the Stage 2 anchor-text keyword filter is skipped.
|
||||
/// Set to false for providers whose search URL already filters by relevance server-side (ejobs.ro, bestjobs.eu).
|
||||
|
||||
@@ -11,4 +11,8 @@ public sealed class JobSearchResultEntity : BaseEntity
|
||||
public string JobText { get; set; } = string.Empty;
|
||||
public int Score { get; set; }
|
||||
public string ResultJson { get; set; } = string.Empty;
|
||||
/// <summary>Email address of the user who triggered the search. Copied from the parent session.</summary>
|
||||
public string? Email { get; set; }
|
||||
/// <summary>Client IP address at link-click time. Copied from the parent session.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ public sealed class JobSearchSessionEntity : BaseEntity
|
||||
public string Status { get; set; } = JobSearchStatus.Pending;
|
||||
public string Keywords { get; set; } = string.Empty;
|
||||
public string? Location { get; set; }
|
||||
/// <summary>Client IP address captured when the user clicked the one-time job-search link. Null for sessions created before this field was added.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
public string? ProviderConfigJson { get; set; }
|
||||
public string Language { get; set; } = "en";
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ public sealed class JobSearchTokenEntity : BaseEntity
|
||||
public bool Used { get; set; }
|
||||
public string Keywords { get; set; } = string.Empty;
|
||||
public string? Location { get; set; }
|
||||
/// <summary>Client IP address captured when the user submitted the CV match request. Null for tokens created before this field was added.</summary>
|
||||
public string? ClientIpAddress { get; set; }
|
||||
}
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CvSearchDbContext))]
|
||||
[Migration("20260608154221_RemoveUseHeadlessBrowser")]
|
||||
partial class RemoveUseHeadlessBrowser
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("cvSearch")
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobProviderEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DisplayOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("InitialKeywordsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)")
|
||||
.HasDefaultValue("[]");
|
||||
|
||||
b.Property<string>("JobLinkContains")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("MaxResults")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(20);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<bool>("RequireKeywordInAnchor")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SearchUrlTemplate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobProviders", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("JobText")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("JobTitle")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("JobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.ToTable("JobSearchResults", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ProviderConfigJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("JobSearchSessions", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobSearchTokens", "cvSearch");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUseHeadlessBrowser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "UseHeadlessBrowser",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobProviders");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "UseHeadlessBrowser",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobProviders",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CvSearchDbContext))]
|
||||
[Migration("20260608161102_AddEmailIpToSessionAndResults")]
|
||||
partial class AddEmailIpToSessionAndResults
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("cvSearch")
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobProviderEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DisplayOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("InitialKeywordsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)")
|
||||
.HasDefaultValue("[]");
|
||||
|
||||
b.Property<string>("JobLinkContains")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("MaxResults")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(20);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<bool>("RequireKeywordInAnchor")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SearchUrlTemplate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobProviders", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobText")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("JobTitle")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("JobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.ToTable("JobSearchResults", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ProviderConfigJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("JobSearchSessions", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobSearchTokens", "cvSearch");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEmailIpToSessionAndResults : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchSessions",
|
||||
type: "nvarchar(45)",
|
||||
maxLength: 45,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchResults",
|
||||
type: "nvarchar(45)",
|
||||
maxLength: 45,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Email",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchResults",
|
||||
type: "nvarchar(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchSessions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchResults");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Email",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchResults");
|
||||
}
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(CvSearchDbContext))]
|
||||
[Migration("20260608161930_AddClientIpToJobSearchTokens")]
|
||||
partial class AddClientIpToJobSearchTokens
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("cvSearch")
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobProviderEntity", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DisplayOrder")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("InitialKeywordsJson")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)")
|
||||
.HasDefaultValue("[]");
|
||||
|
||||
b.Property<string>("JobLinkContains")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<int>("MaxResults")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(20);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<bool>("RequireKeywordInAnchor")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("SearchUrlTemplate")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobProviders", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobText")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("JobTitle")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<string>("JobUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SessionId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SessionId");
|
||||
|
||||
b.ToTable("JobSearchResults", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ProviderConfigJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("nvarchar(32)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.ToTable("JobSearchSessions", "cvSearch");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Keywords")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)")
|
||||
.HasDefaultValue("");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(8)
|
||||
.HasColumnType("nvarchar(8)")
|
||||
.HasDefaultValue("en");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("Used")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobSearchTokens", "cvSearch");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using CvSearch.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvSearch.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddClientIpToJobSearchTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchTokens",
|
||||
type: "nvarchar(45)",
|
||||
maxLength: 45,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ClientIpAddress",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "JobSearchTokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,11 +69,6 @@ namespace CvSearch.Data.Migrations
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<bool>("UseHeadlessBrowser")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("JobProviders", "cvSearch");
|
||||
@@ -85,11 +80,19 @@ namespace CvSearch.Data.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobText")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
@@ -134,6 +137,10 @@ namespace CvSearch.Data.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
@@ -190,6 +197,10 @@ namespace CvSearch.Data.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class SmtpEmailDispatcher
|
||||
if (!string.IsNullOrWhiteSpace(req.ReplyTo))
|
||||
msg.ReplyTo.Add(MailboxAddress.Parse(req.ReplyTo));
|
||||
|
||||
msg.Subject = $"[{_environmentName}] {req.Subject}".Trim();
|
||||
msg.Subject = req.Subject.Trim();
|
||||
|
||||
var shellStart = _templates.Get("email.html-shell.start", "*");
|
||||
var shellEnd = _templates.Get("email.html-shell.end", "*");
|
||||
|
||||
@@ -17,4 +17,10 @@ public sealed class FetchPageRequest
|
||||
/// Identifies the calling service for audit purposes (e.g. <c>cv-matcher-api</c>, <c>cv-search-job</c>).
|
||||
/// </summary>
|
||||
public string CallerService { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional reference to the job search session that triggered this fetch.
|
||||
/// Stored on <c>pageFetcher.PageFetches</c> for cross-schema audit queries.
|
||||
/// </summary>
|
||||
public string? JobSearchSessionId { get; set; }
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ public sealed class PageFetcherService
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Url = request.Url,
|
||||
CallerService = request.CallerService ?? string.Empty,
|
||||
JobSearchSessionId = request.JobSearchSessionId,
|
||||
HttpStatusCode = statusCode,
|
||||
Html = html,
|
||||
Text = text,
|
||||
|
||||
@@ -31,4 +31,10 @@ public sealed class PageFetchEntity : BaseEntity
|
||||
|
||||
/// <summary>Exception message when <see cref="Success"/> is <c>false</c>.</summary>
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional reference to the <c>cvSearch.JobSearchSessions</c> row that triggered this fetch.
|
||||
/// Null for fetches not originating from a job search session (e.g. direct CV-to-job matches).
|
||||
/// </summary>
|
||||
public string? JobSearchSessionId { get; set; }
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using PageFetcher.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PageFetcher.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(PageFetchDbContext))]
|
||||
[Migration("20260608165542_AddJobSearchSessionId")]
|
||||
partial class AddJobSearchSessionId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("pageFetcher")
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("PageFetcher.Data.Entities.PageFetchEntity", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("CallerService")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<long>("DurationMs")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("Html")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int?>("HttpStatusCode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("JobSearchSessionId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Success")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Text")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("JobSearchSessionId");
|
||||
|
||||
b.HasIndex("Url");
|
||||
|
||||
b.ToTable("PageFetches", "pageFetcher");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using PageFetcher.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PageFetcher.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddJobSearchSessionId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "JobSearchSessionId",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "PageFetches",
|
||||
type: "nvarchar(64)",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PageFetches_JobSearchSessionId",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "PageFetches",
|
||||
column: "JobSearchSessionId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PageFetches_JobSearchSessionId",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "PageFetches");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "JobSearchSessionId",
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "PageFetches");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,10 @@ namespace PageFetcher.Data.Migrations
|
||||
b.Property<int?>("HttpStatusCode")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("JobSearchSessionId")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("Success")
|
||||
.HasColumnType("bit");
|
||||
|
||||
@@ -69,6 +73,8 @@ namespace PageFetcher.Data.Migrations
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("JobSearchSessionId");
|
||||
|
||||
b.HasIndex("Url");
|
||||
|
||||
b.ToTable("PageFetches", "pageFetcher");
|
||||
|
||||
@@ -36,8 +36,11 @@ public sealed class PageFetchDbContext : DbContext
|
||||
entity.Property(x => x.Html).IsRequired();
|
||||
entity.Property(x => x.Text).IsRequired();
|
||||
entity.Property(x => x.ErrorMessage).HasMaxLength(2000);
|
||||
entity.Property(x => x.JobSearchSessionId).HasMaxLength(64);
|
||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
entity.HasIndex(x => x.JobSearchSessionId);
|
||||
|
||||
entity.HasIndex(x => x.Url);
|
||||
entity.HasIndex(x => x.CreatedAt);
|
||||
});
|
||||
|
||||
@@ -72,7 +72,6 @@ public sealed class HtmlJobSearcher
|
||||
var fetchResponse = await _pageFetcher.FetchAsync(new FetchPageRequest
|
||||
{
|
||||
Url = searchUrl,
|
||||
WaitFor = provider.UseHeadlessBrowser ? "networkidle" : "domcontentloaded",
|
||||
CallerService = "cv-search-job"
|
||||
}, ct);
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ public sealed class CvSearchJobTask : IJobTask
|
||||
{
|
||||
Url = url,
|
||||
WaitFor = "domcontentloaded",
|
||||
CallerService = "cv-search-job"
|
||||
CallerService = "cv-search-job",
|
||||
JobSearchSessionId = session.Id
|
||||
}, ct);
|
||||
|
||||
if (!fetchResponse.Success || string.IsNullOrWhiteSpace(fetchResponse.Text))
|
||||
@@ -217,9 +218,11 @@ public sealed class CvSearchJobTask : IJobTask
|
||||
ProviderName = GuessProvider(url, providers),
|
||||
JobUrl = url,
|
||||
JobTitle = matchResult.Summary.Split('.').FirstOrDefault()?.Trim() ?? title,
|
||||
JobText = string.Empty,
|
||||
JobText = jobText,
|
||||
Score = matchResult.Score,
|
||||
ResultJson = JsonSerializer.Serialize(matchResult, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
|
||||
Email = session.Email,
|
||||
ClientIpAddress = session.ClientIpAddress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user