refinement phase 3
This commit is contained in:
+23
-7
@@ -40,17 +40,33 @@ internal sealed class AuditLogger(
|
||||
}
|
||||
|
||||
public async ValueTask<PagedResult<AuditLogDto>> GetTrailAsync(
|
||||
string entityType,
|
||||
string entityId,
|
||||
string? entityType,
|
||||
string? entityId,
|
||||
int? actorUserId,
|
||||
string? action,
|
||||
System.DateTimeOffset? from,
|
||||
System.DateTimeOffset? to,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = db.Set<AuditLog>()
|
||||
.AsNoTracking()
|
||||
.Where(a => a.EntityType == entityType && a.EntityId == entityId)
|
||||
// Id is a monotonic identity → newest-first and deterministic (no timestamp ties).
|
||||
.OrderByDescending(a => a.Id);
|
||||
var filtered = db.Set<AuditLog>().AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(entityType))
|
||||
filtered = filtered.Where(a => a.EntityType == entityType);
|
||||
if (!string.IsNullOrWhiteSpace(entityId))
|
||||
filtered = filtered.Where(a => a.EntityId == entityId);
|
||||
if (actorUserId is { } actor)
|
||||
filtered = filtered.Where(a => a.ActorUserId == actor);
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
filtered = filtered.Where(a => a.Action == action);
|
||||
if (from is { } f)
|
||||
filtered = filtered.Where(a => a.OccurredAt >= f);
|
||||
if (to is { } t)
|
||||
filtered = filtered.Where(a => a.OccurredAt <= t);
|
||||
|
||||
// Id is a monotonic identity → newest-first and deterministic (no timestamp ties).
|
||||
var query = filtered.OrderByDescending(a => a.Id);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ internal sealed class PlatformConfigService(ApplicationDbContext db, ICacheServi
|
||||
async ct => await db.Set<PlatformConfig>()
|
||||
.AsNoTracking()
|
||||
.Where(c => c.Key == key)
|
||||
.Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description))
|
||||
.Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description, c.ModifiedAt ?? c.CreatedAt, c.ModifiedById ?? c.CreatedById))
|
||||
.FirstOrDefaultAsync(ct),
|
||||
CacheTtl,
|
||||
cancellationToken);
|
||||
@@ -61,7 +61,7 @@ internal sealed class PlatformConfigService(ApplicationDbContext db, ICacheServi
|
||||
var items = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description))
|
||||
.Select(c => new PlatformConfigDto(c.Key, c.Value, c.DataType, c.Description, c.ModifiedAt ?? c.CreatedAt, c.ModifiedById ?? c.CreatedById))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<PlatformConfigDto>(items, total, page, pageSize);
|
||||
|
||||
+26
-11
@@ -54,13 +54,14 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr
|
||||
public async Task ReindexNurseAsync(NurseProfile profile, VerificationStatus? verificationStatus, CancellationToken cancellationToken)
|
||||
{
|
||||
var status = verificationStatus ?? await LoadStatusAsync(profile.Id, cancellationToken);
|
||||
var gender = await LoadGenderAsync(profile.Id, cancellationToken);
|
||||
var identity = await LoadIdentityAsync(profile.Id, cancellationToken);
|
||||
|
||||
// Bookability + aggregates come from the tracked profile (its just-changed values); gender is stable
|
||||
// for these triggers so it is read from the database.
|
||||
// Bookability + aggregates come from the tracked profile (its just-changed values); gender + name are
|
||||
// stable for these triggers so they are read from the database. The avatar is on the tracked profile.
|
||||
var ctx = new NurseContext(
|
||||
profile.IsVerified, profile.IsAcceptingBookings, gender,
|
||||
profile.AverageRating, profile.TotalReviews, profile.TotalCompletedBookings);
|
||||
profile.IsVerified, profile.IsAcceptingBookings, identity.Gender,
|
||||
profile.AverageRating, profile.TotalReviews, profile.TotalCompletedBookings,
|
||||
identity.Name, profile.AvatarUrl);
|
||||
var bookable = NurseBookable(profile.IsVerified, profile.IsAcceptingBookings, status);
|
||||
|
||||
var areas = await LoadActiveAreasAsync(profile.Id, cancellationToken);
|
||||
@@ -197,6 +198,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr
|
||||
existing.Price = variant.Price;
|
||||
existing.PriceUnit = variant.PriceUnit;
|
||||
existing.NurseGender = ctx.Gender;
|
||||
existing.NurseName = ctx.NurseName;
|
||||
existing.AvatarUrl = ctx.AvatarUrl;
|
||||
existing.AverageRating = ctx.AverageRating;
|
||||
existing.TotalReviews = ctx.TotalReviews;
|
||||
existing.TotalCompletedBookings = ctx.TotalCompletedBookings;
|
||||
@@ -218,6 +221,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr
|
||||
CityId = area.CityId,
|
||||
DistrictId = area.DistrictId,
|
||||
NurseGender = ctx.Gender,
|
||||
NurseName = ctx.NurseName,
|
||||
AvatarUrl = ctx.AvatarUrl,
|
||||
AverageRating = ctx.AverageRating,
|
||||
TotalReviews = ctx.TotalReviews,
|
||||
TotalCompletedBookings = ctx.TotalCompletedBookings,
|
||||
@@ -248,14 +253,21 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr
|
||||
.Where(p => p.Id == nurseId)
|
||||
.Select(p => new NurseContext(
|
||||
p.IsVerified, p.IsAcceptingBookings, p.User.Gender,
|
||||
p.AverageRating, p.TotalReviews, p.TotalCompletedBookings))
|
||||
p.AverageRating, p.TotalReviews, p.TotalCompletedBookings,
|
||||
((p.User.Name ?? "") + " " + (p.User.FamilyName ?? "")).Trim(), p.AvatarUrl))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
private async Task<string> LoadGenderAsync(long nurseId, CancellationToken cancellationToken)
|
||||
=> await db.Set<NurseProfile>()
|
||||
private async Task<NurseIdentity> LoadIdentityAsync(long nurseId, CancellationToken cancellationToken)
|
||||
{
|
||||
var identity = await db.Set<NurseProfile>()
|
||||
.Where(p => p.Id == nurseId)
|
||||
.Select(p => p.User.Gender)
|
||||
.FirstOrDefaultAsync(cancellationToken) ?? string.Empty;
|
||||
.Select(p => new NurseIdentity(
|
||||
p.User.Gender ?? string.Empty,
|
||||
((p.User.Name ?? "") + " " + (p.User.FamilyName ?? "")).Trim()))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
// NurseIdentity is a struct — FirstOrDefaultAsync yields default (null fields) when no row exists.
|
||||
return new NurseIdentity(identity.Gender ?? string.Empty, identity.Name ?? string.Empty);
|
||||
}
|
||||
|
||||
private Task<VerificationStatus?> LoadStatusAsync(long nurseId, CancellationToken cancellationToken)
|
||||
=> db.Set<NurseVerification>()
|
||||
@@ -281,5 +293,8 @@ internal sealed class SearchIndexMaintainer(ApplicationDbContext db, IDateTimePr
|
||||
|
||||
private sealed record NurseContext(
|
||||
bool IsVerified, bool IsAcceptingBookings, string Gender,
|
||||
decimal AverageRating, int TotalReviews, int TotalCompletedBookings);
|
||||
decimal AverageRating, int TotalReviews, int TotalCompletedBookings,
|
||||
string NurseName, string AvatarUrl);
|
||||
|
||||
private readonly record struct NurseIdentity(string Gender, string Name);
|
||||
}
|
||||
|
||||
+7
-3
@@ -54,19 +54,23 @@ internal sealed class SqlNurseSearch(ApplicationDbContext db) : INurseSearch
|
||||
.Take(criteria.PageSize)
|
||||
.Select(r => new Row(
|
||||
r.VariantId, r.NurseId, r.ServiceCategoryId, r.Price, r.PriceUnit, r.NurseGender,
|
||||
r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId))
|
||||
r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId,
|
||||
r.NurseName, r.AvatarUrl))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Format price to a digit string in memory (no long.ToString translation required in SQL).
|
||||
// DistanceKm is null: the covering index carries no coordinate, so distance is not derivable here.
|
||||
var items = rows.Select(r => new NurseSearchResultDto(
|
||||
r.VariantId, r.NurseId, r.ServiceCategoryId,
|
||||
r.Price.ToString(CultureInfo.InvariantCulture), r.PriceUnit, r.NurseGender,
|
||||
r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId)).ToList();
|
||||
r.AverageRating, r.TotalReviews, r.TotalCompletedBookings, r.CityId, r.DistrictId,
|
||||
r.NurseName, r.AvatarUrl, null)).ToList();
|
||||
|
||||
return new PagedResult<NurseSearchResultDto>(items, total, criteria.Page, criteria.PageSize);
|
||||
}
|
||||
|
||||
private sealed record Row(
|
||||
long VariantId, long NurseId, long ServiceCategoryId, long Price, string PriceUnit, string NurseGender,
|
||||
decimal AverageRating, int TotalReviews, int TotalCompletedBookings, long CityId, long? DistrictId);
|
||||
decimal AverageRating, int TotalReviews, int TotalCompletedBookings, long CityId, long? DistrictId,
|
||||
string NurseName, string AvatarUrl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user