Multi-role ads: parse all roles + fan-out publish one listing per role
An ad like «استخدام پرستار سالمند و کودک و همراه بیمار» names several roles; we kept only the first. Now: - Parser collects ALL roles (ParsedListing.RoleNames): exact taxonomy matches (substring-deduped so پرستار⊂پرستار سالمندان) plus synonyms (سالمند→پرستار سالمندان, کودک/همراه بیمار→پرستار, اتاق عمل→تکنسین اتاق عمل…), capped at 4. - Ingestion publishes one Shift/Job/Talent per resolved role (AI role + parser roles, distinct, capped), so each role is independently browsable and filterable. RawListing dedupe is unchanged (one raw → N posts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -161,11 +161,21 @@ public class IngestionService
|
||||
List<Role> roles, List<City> cities, List<District> districts, List<Facility> facilities)
|
||||
{
|
||||
var d = ai?.Data;
|
||||
var roleName = d?.Role ?? parsed.RoleName;
|
||||
var cityName = d?.City ?? parsed.CityName;
|
||||
var districtName = d?.District ?? parsed.DistrictName;
|
||||
|
||||
var role = roles.FirstOrDefault(r => r.Name == roleName) ?? roles.First();
|
||||
// One ad can name several roles («پرستار سالمند و کودک و همراه بیمار») — resolve them all
|
||||
// and publish one listing per role so each is browsable/filterable. Capped to avoid spam.
|
||||
var roleNames = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(d?.Role)) roleNames.Add(d!.Role!.Trim());
|
||||
roleNames.AddRange(parsed.RoleNames);
|
||||
if (parsed.RoleName is not null) roleNames.Add(parsed.RoleName);
|
||||
var pubRoles = roleNames
|
||||
.Select(n => roles.FirstOrDefault(r => r.Name == n))
|
||||
.Where(r => r is not null).Cast<Role>()
|
||||
.Distinct().Take(4).ToList();
|
||||
if (pubRoles.Count == 0) pubRoles.Add(roles.First());
|
||||
|
||||
var city = cities.FirstOrDefault(c => c.Name == cityName)
|
||||
?? cities.FirstOrDefault(c => c.IsActive) ?? cities.First();
|
||||
var district = districts.FirstOrDefault(x => x.Name == districtName && x.CityId == city.Id);
|
||||
@@ -178,22 +188,23 @@ public class IngestionService
|
||||
// Prefer the AI's tags when present, else the heuristic parser.
|
||||
var tPay = d?.PayAmount ?? parsed.PayAmount;
|
||||
var tShare = d?.SharePercent ?? parsed.SharePercent;
|
||||
_db.TalentListings.Add(new TalentListing
|
||||
{
|
||||
Role = role, City = city, DistrictId = district?.Id,
|
||||
PersonName = !string.IsNullOrWhiteSpace(d?.PersonName) ? d!.PersonName!.Trim() : parsed.PersonName,
|
||||
YearsExperience = d?.YearsExperience ?? parsed.YearsExperience,
|
||||
IsLicensed = d?.IsLicensed ?? parsed.IsLicensed,
|
||||
AreaNote = parsed.AreaNote,
|
||||
Availability = MapEmployment(d?.EmploymentType, parsed.EmploymentType),
|
||||
Gender = parsed.Gender,
|
||||
PayType = tShare is not null && tPay is null ? PayType.Percentage
|
||||
: tPay is null ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = tPay, SharePercent = tShare,
|
||||
Phone = !string.IsNullOrWhiteSpace(d?.Phone) ? d!.Phone!.Trim() : parsed.Phone,
|
||||
Description = raw.RawText,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
foreach (var role in pubRoles)
|
||||
_db.TalentListings.Add(new TalentListing
|
||||
{
|
||||
Role = role, City = city, DistrictId = district?.Id,
|
||||
PersonName = !string.IsNullOrWhiteSpace(d?.PersonName) ? d!.PersonName!.Trim() : parsed.PersonName,
|
||||
YearsExperience = d?.YearsExperience ?? parsed.YearsExperience,
|
||||
IsLicensed = d?.IsLicensed ?? parsed.IsLicensed,
|
||||
AreaNote = parsed.AreaNote,
|
||||
Availability = MapEmployment(d?.EmploymentType, parsed.EmploymentType),
|
||||
Gender = parsed.Gender,
|
||||
PayType = tShare is not null && tPay is null ? PayType.Percentage
|
||||
: tPay is null ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = tPay, SharePercent = tShare,
|
||||
Phone = !string.IsNullOrWhiteSpace(d?.Phone) ? d!.Phone!.Trim() : parsed.Phone,
|
||||
Description = raw.RawText,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
raw.Status = RawListingStatus.Normalized;
|
||||
return;
|
||||
}
|
||||
@@ -217,31 +228,33 @@ public class IngestionService
|
||||
|
||||
if (kindStr.Contains("job") || kindStr.Contains("استخدام"))
|
||||
{
|
||||
_db.JobOpenings.Add(new JobOpening
|
||||
{
|
||||
Facility = facility, Role = role,
|
||||
Title = !string.IsNullOrWhiteSpace(d?.Title) ? d!.Title!.Trim() : $"استخدام {role.Name}",
|
||||
EmploymentType = MapEmployment(d?.EmploymentType, parsed.EmploymentType),
|
||||
SalaryMin = parsed.PayAmount,
|
||||
Description = raw.RawText, Status = ShiftStatus.Open, Source = ShiftSource.Aggregated,
|
||||
SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
foreach (var role in pubRoles)
|
||||
_db.JobOpenings.Add(new JobOpening
|
||||
{
|
||||
Facility = facility, Role = role,
|
||||
Title = !string.IsNullOrWhiteSpace(d?.Title) && pubRoles.Count == 1 ? d!.Title!.Trim() : $"استخدام {role.Name}",
|
||||
EmploymentType = MapEmployment(d?.EmploymentType, parsed.EmploymentType),
|
||||
SalaryMin = parsed.PayAmount,
|
||||
Description = raw.RawText, Status = ShiftStatus.Open, Source = ShiftSource.Aggregated,
|
||||
SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var st = MapShiftType(d?.ShiftType, parsed.ShiftType);
|
||||
var (start, end) = DefaultTimes(st);
|
||||
_db.Shifts.Add(new Shift
|
||||
{
|
||||
Facility = facility, Role = role,
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1),
|
||||
StartTime = start, EndTime = end, ShiftType = st,
|
||||
SpecialtyRequired = role.Name, Description = raw.RawText,
|
||||
PayType = parsed.SharePercent is not null && parsed.PayAmount is null ? PayType.Percentage
|
||||
: parsed.PayAmount is null ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = parsed.PayAmount, SharePercent = parsed.SharePercent,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
foreach (var role in pubRoles)
|
||||
_db.Shifts.Add(new Shift
|
||||
{
|
||||
Facility = facility, Role = role,
|
||||
Date = DateOnly.FromDateTime(DateTime.UtcNow).AddDays(1),
|
||||
StartTime = start, EndTime = end, ShiftType = st,
|
||||
SpecialtyRequired = role.Name, Description = raw.RawText,
|
||||
PayType = parsed.SharePercent is not null && parsed.PayAmount is null ? PayType.Percentage
|
||||
: parsed.PayAmount is null ? PayType.Negotiable : PayType.PerShift,
|
||||
PayAmount = parsed.PayAmount, SharePercent = parsed.SharePercent,
|
||||
Status = ShiftStatus.Open, Source = ShiftSource.Aggregated, SourceUrl = raw.SourceUrl,
|
||||
});
|
||||
}
|
||||
raw.Status = RawListingStatus.Normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user