Add in-place role-fix for existing «پزشک عمومی»-mislabeled listings
CI/CD / CI · dotnet build (push) Successful in 45s
CI/CD / Deploy · hamkadr (push) Successful in 2m5s

RecorrectDoctorRolesAsync (+ admin button «اصلاح نقش»): re-runs the keyword parser + doctor-role
guard over the stored text of existing aggregated listings currently labeled «پزشک عمومی», and
corrects RoleId + the generic title in place when the text actually names a more specific role
(dentist, «متخصص», lab, …). No AI call, no delete/recreate — IDs and indexed URLs unchanged, only
GP-labeled rows touched. Cleans up the dentist/ENT/«متخصص غدد» mislabels already published.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 18:06:22 +03:30
parent fbf8deaa8c
commit 7bbb4e385e
3 changed files with 69 additions and 0 deletions
@@ -81,6 +81,15 @@
🏥 ادغام مراکز تکراری و حذف مراکز بی‌نام 🏥 ادغام مراکز تکراری و حذف مراکز بی‌نام
</button> </button>
</form> </form>
<form method="post">
<button type="submit" asp-page-handler="RecorrectRoles" class="btn btn-primary btn-block" style="margin-top:10px;">
🩺 اصلاح نقشِ آگهی‌های «پزشک عمومی» (دندانپزشک/متخصص و …)
</button>
</form>
<p class="muted" style="font-size:11px; margin:6px 0 0;">
آگهی‌هایی که هوش مصنوعی به اشتباه «پزشک عمومی» زده ولی متنشان نقش دیگری دارد، از روی متن اصلاح می‌شوند (درجا، بدون تغییر شناسه/آدرس).
</p>
<p class="muted" style="font-size:11px; margin:6px 0 0;"> <p class="muted" style="font-size:11px; margin:6px 0 0;">
مراکز تکراری (با تطبیقِ فارسی) در یک رکورد ادغام و مراکزِ بدونِ نامِ واقعی به «نامشخص» منتقل می‌شوند. آگهی‌ها حفظ می‌شوند؛ فقط مراکزِ جمع‌آوری‌شده و مدیریت‌نشده پاک می‌شوند. مراکز تکراری (با تطبیقِ فارسی) در یک رکورد ادغام و مراکزِ بدونِ نامِ واقعی به «نامشخص» منتقل می‌شوند. آگهی‌ها حفظ می‌شوند؛ فقط مراکزِ جمع‌آوری‌شده و مدیریت‌نشده پاک می‌شوند.
</p> </p>
@@ -157,6 +157,15 @@ public class IndexModel : PageModel
return RedirectToPage(); return RedirectToPage();
} }
/// <summary>Fix existing aggregated listings the AI mislabeled «پزشک عمومی» (dentist/specialist/…)
/// in place from their stored text — no AI, no ID/URL change.</summary>
public async Task<IActionResult> OnPostRecorrectRolesAsync()
{
var n = await _ingest.RecorrectDoctorRolesAsync();
IngestMessage = $"اصلاح نقش: {n} آگهیِ «پزشک عمومی» که در واقع نقش دیگری بود (دندانپزشک، متخصص و …) از روی متن آگهی اصلاح شد. بدون تغییر شناسه یا آدرس صفحه.";
return RedirectToPage();
}
private async Task LoadAsync() private async Task LoadAsync()
{ {
Queue = await _db.RawListings Queue = await _db.RawListings
@@ -522,6 +522,57 @@ public class IngestionService
return (merged, cleaned); return (merged, cleaned);
} }
/// <summary>
/// In-place fix for EXISTING aggregated listings the AI mislabeled «پزشک عمومی» when the ad text
/// actually names a more specific role (dentist, endocrinologist/«متخصص», lab, …). Re-runs the
/// keyword parser + the same doctor-role guard over the stored text and updates RoleId (and the
/// generic «استخدام پزشک عمومی» title) IN PLACE — no AI call, no delete/recreate, so IDs and
/// indexed URLs are untouched. Only ever changes rows currently labeled «پزشک عمومی». Returns the
/// number corrected.
/// </summary>
public async Task<int> RecorrectDoctorRolesAsync(CancellationToken ct = default)
{
var roles = await _db.Roles.ToListAsync(ct);
var roleNames = roles.Select(r => r.Name).ToList();
var cityNames = await _db.Cities.Select(c => c.Name).ToListAsync(ct);
var districtNames = await _db.Districts.Select(d => d.Name).ToListAsync(ct);
var gp = roles.FirstOrDefault(r => r.Name == "پزشک عمومی");
if (gp is null) return 0;
Role? Corrected(string? text)
{
var parsed = _parser.Parse(text ?? "", roleNames, cityNames, districtNames);
var specific = parsed.RoleNames.FirstOrDefault(n => NormalizeFa(n) != NormalizeFa("پزشک عمومی"));
if (specific is not null) return ResolveOrCreateRole(roles, specific, null);
if (LooksSpecialist(text)) return ResolveOrCreateRole(roles, "پزشک متخصص", "پزشک");
return null;
}
int fixedCount = 0;
var jobs = await _db.JobOpenings
.Where(j => j.Status == ShiftStatus.Open && j.Source == ShiftSource.Aggregated && j.RoleId == gp.Id)
.ToListAsync(ct);
foreach (var j in jobs)
{
if (Corrected(j.Description) is { } nr && nr.Id != j.RoleId)
{
if (string.IsNullOrWhiteSpace(j.Title) || j.Title == "استخدام پزشک عمومی") j.Title = $"استخدام {nr.Name}";
j.RoleId = nr.Id; fixedCount++;
}
}
var talent = await _db.TalentListings
.Where(t => t.Status == ShiftStatus.Open && t.Source == ShiftSource.Aggregated && t.RoleId == gp.Id)
.ToListAsync(ct);
foreach (var t in talent)
if (Corrected(t.Description) is { } nr && nr.Id != t.RoleId) { t.RoleId = nr.Id; fixedCount++; }
if (fixedCount > 0) await _db.SaveChangesAsync(ct);
_log.LogInformation("Recorrected {N} «پزشک عمومی»-mislabeled aggregated listings.", fixedCount);
return fixedCount;
}
private static string DigitsOnly(string s) => new(HtmlUtil.ToLatinDigits(s).Where(char.IsDigit).ToArray()); private static string DigitsOnly(string s) => new(HtmlUtil.ToLatinDigits(s).Where(char.IsDigit).ToArray());
private static (RawListingStatus status, string? reason, int confidence) Decide( private static (RawListingStatus status, string? reason, int confidence) Decide(