490821a637
- Talent «آماده به کار» now has its own freshness window (21 days, vs 30 for jobs) since availability goes stale fast; archiver, browse, and home use TalentCutoffUtc. - Expired/filled job openings and past/filled shifts now emit robots noindex so Google drops dead listings instead of keeping soft-404 pages. (Talent details were already noindex.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.5 KiB
C#
60 lines
2.5 KiB
C#
using JobsMedical.Web.Data;
|
|
using JobsMedical.Web.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace JobsMedical.Web.Services.Scraping;
|
|
|
|
/// <summary>
|
|
/// Centralizes "what counts as a live listing". Jobs have no end date, so they go stale after a
|
|
/// window; shifts expire once their date has passed. Public screens hide anything outside these
|
|
/// rules, and <see cref="ListingArchiver"/> flips stale Open rows to Expired so dashboards/DB agree.
|
|
/// </summary>
|
|
public static class ListingPolicy
|
|
{
|
|
/// <summary>A job opening older than this (since posting) is treated as stale and hidden.</summary>
|
|
public const int JobFreshnessDays = 30;
|
|
|
|
/// <summary>«آماده به کار» goes stale faster — the person is usually hired within a few weeks.</summary>
|
|
public const int TalentFreshnessDays = 21;
|
|
|
|
public static DateTime JobCutoffUtc => DateTime.UtcNow.AddDays(-JobFreshnessDays);
|
|
public static DateTime TalentCutoffUtc => DateTime.UtcNow.AddDays(-TalentFreshnessDays);
|
|
}
|
|
|
|
/// <summary>Sweeps stale listings into the Expired state (archive). Idempotent and cheap.</summary>
|
|
public class ListingArchiver
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ILogger<ListingArchiver> _log;
|
|
|
|
public ListingArchiver(AppDbContext db, ILogger<ListingArchiver> log)
|
|
{
|
|
_db = db;
|
|
_log = log;
|
|
}
|
|
|
|
public async Task<int> ArchiveStaleAsync(CancellationToken ct = default)
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var jobCutoff = ListingPolicy.JobCutoffUtc;
|
|
|
|
var expiredShifts = await _db.Shifts
|
|
.Where(s => s.Status == ShiftStatus.Open && s.Date < today)
|
|
.ExecuteUpdateAsync(u => u.SetProperty(s => s.Status, ShiftStatus.Expired), ct);
|
|
|
|
var expiredJobs = await _db.JobOpenings
|
|
.Where(j => j.Status == ShiftStatus.Open && j.CreatedAt < jobCutoff)
|
|
.ExecuteUpdateAsync(u => u.SetProperty(j => j.Status, ShiftStatus.Expired), ct);
|
|
|
|
var talentCutoff = ListingPolicy.TalentCutoffUtc;
|
|
var expiredTalent = await _db.TalentListings
|
|
.Where(t => t.Status == ShiftStatus.Open && t.CreatedAt < talentCutoff)
|
|
.ExecuteUpdateAsync(u => u.SetProperty(t => t.Status, ShiftStatus.Expired), ct);
|
|
|
|
if (expiredShifts + expiredJobs + expiredTalent > 0)
|
|
_log.LogInformation("Archived {Shifts} shifts + {Jobs} jobs + {Talent} talent as expired",
|
|
expiredShifts, expiredJobs, expiredTalent);
|
|
return expiredShifts + expiredJobs + expiredTalent;
|
|
}
|
|
}
|