fix: 员工排班移除mock与兜底逻辑
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 43s

This commit is contained in:
2026-02-20 09:02:52 +08:00
parent 145be01bfc
commit 069d7de05e
2 changed files with 74 additions and 136 deletions

View File

@@ -154,7 +154,15 @@ public sealed class StoreStaffScheduleDto
/// <summary>
/// Templates。
/// </summary>
public StoreShiftTemplatesDto Templates { get; set; } = new();
public StoreShiftTemplatesDto? Templates { get; set; }
/// <summary>
/// IsTemplateConfigured。
/// </summary>
public bool IsTemplateConfigured { get; set; }
/// <summary>
/// IsScheduleConfigured。
/// </summary>
public bool IsScheduleConfigured { get; set; }
/// <summary>
/// Schedules。
/// </summary>

View File

@@ -135,9 +135,6 @@ public sealed class StoreStaffController(
await dbContext.SaveChangesAsync(cancellationToken);
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
await EnsureStaffScheduleAsync(tenantId, parsedStoreId, entity, template, cancellationToken);
return ApiResponse<StoreStaffItemDto>.Ok(MapStaff(entity));
}
@@ -186,7 +183,7 @@ public sealed class StoreStaffController(
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, parsedStoreId, cancellationToken);
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
var staffs = await dbContext.MerchantStaff
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
@@ -205,8 +202,10 @@ public sealed class StoreStaffController(
var schedules = staffs.Select(staff =>
{
var shifts = scheduleMap.TryGetValue(staff.Id, out var rows)
? NormalizeRowsToShifts(rows, staff, template)
: CreateDefaultWeekByRole(staff.RoleType, template);
? template is null
? []
: NormalizeRowsToShifts(rows, template)
: [];
if (staff.Status == StaffStatus.Resigned)
{
@@ -225,6 +224,8 @@ public sealed class StoreStaffController(
StoreId = parsedStoreId.ToString(),
WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(weekStartDate),
Templates = template,
IsTemplateConfigured = template is not null,
IsScheduleConfigured = scheduleRows.Count > 0,
Schedules = schedules
});
}
@@ -298,19 +299,23 @@ public sealed class StoreStaffController(
throw new BusinessException(ErrorCodes.BadRequest, "员工不存在");
}
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
if (template is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "请先配置班次模板");
}
var existingRows = await dbContext.StoreStaffWeeklySchedules
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId && x.StaffId == parsedStaffId)
.ToListAsync(cancellationToken);
var fallback = NormalizeRowsToShifts(existingRows, staff, template);
var fallback = NormalizeRowsToShifts(existingRows, template);
var shifts = staff.Status == StaffStatus.Resigned
? CreateOffWeekShifts()
: NormalizeShifts(request.Shifts, fallback, template);
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
ToWeeklyEntities(parsedStoreId, parsedStaffId, shifts),
ToWeeklyEntities(parsedStoreId, parsedStaffId, shifts, template),
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
@@ -343,7 +348,11 @@ public sealed class StoreStaffController(
.ToListAsync(cancellationToken);
var staffMap = staffs.ToDictionary(x => x.Id);
var template = await GetOrCreateTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
var template = await GetTemplateDtoAsync(tenantId, parsedStoreId, cancellationToken);
if (template is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "请先配置班次模板");
}
var existingRows = await dbContext.StoreStaffWeeklySchedules
.Where(x => x.TenantId == tenantId && x.StoreId == parsedStoreId)
.ToListAsync(cancellationToken);
@@ -351,7 +360,7 @@ public sealed class StoreStaffController(
.GroupBy(x => x.StaffId)
.ToDictionary(
x => x.Key,
x => NormalizeRowsToShifts(x.ToList(), staffMap.GetValueOrDefault(x.Key), template));
x => NormalizeRowsToShifts(x.ToList(), template));
var incomingMap = new Dictionary<long, List<StaffDayShiftDto>>();
foreach (var schedule in request.Schedules ?? [])
@@ -367,7 +376,7 @@ public sealed class StoreStaffController(
continue;
}
var fallback = existingMap.GetValueOrDefault(staffId.Value) ?? CreateDefaultWeekByRole(staff.RoleType, template);
var fallback = existingMap.GetValueOrDefault(staffId.Value) ?? [];
incomingMap[staffId.Value] = NormalizeShifts(schedule.Shifts, fallback, template);
}
@@ -385,7 +394,7 @@ public sealed class StoreStaffController(
}
else
{
shifts = existingMap.GetValueOrDefault(staff.Id) ?? CreateDefaultWeekByRole(staff.RoleType, template);
shifts = existingMap.GetValueOrDefault(staff.Id) ?? [];
}
finalSchedules.Add(new StaffScheduleDto
@@ -397,7 +406,7 @@ public sealed class StoreStaffController(
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
var entities = finalSchedules
.SelectMany(x => ToWeeklyEntities(parsedStoreId, long.Parse(x.StaffId), x.Shifts))
.SelectMany(x => ToWeeklyEntities(parsedStoreId, long.Parse(x.StaffId), x.Shifts, template))
.ToList();
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(entities, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
@@ -407,6 +416,8 @@ public sealed class StoreStaffController(
StoreId = parsedStoreId.ToString(),
WeekStartDate = StoreApiHelpers.ResolveWeekStartDate(null),
Templates = template,
IsTemplateConfigured = true,
IsScheduleConfigured = entities.Count > 0,
Schedules = finalSchedules
});
}
@@ -451,7 +462,11 @@ public sealed class StoreStaffController(
});
}
var sourceTemplate = await GetOrCreateTemplateDtoAsync(tenantId, sourceStoreId, cancellationToken);
var sourceTemplate = await GetTemplateDtoAsync(tenantId, sourceStoreId, cancellationToken);
if (sourceTemplate is null)
{
throw new BusinessException(ErrorCodes.BadRequest, "源门店未配置班次模板,无法复制");
}
var sourceRows = await dbContext.StoreStaffWeeklySchedules
.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.StoreId == sourceStoreId)
@@ -465,14 +480,14 @@ public sealed class StoreStaffController(
.GroupBy(x => x.StaffId)
.ToDictionary(
x => x.Key,
x => NormalizeRowsToShifts(x.ToList(), sourceStaffMap.GetValueOrDefault(x.Key), sourceTemplate));
x => NormalizeRowsToShifts(x.ToList(), sourceTemplate));
var sourceScheduleSequence = sourceStaffs
.OrderBy(x => x.CreatedAt)
.ThenBy(x => x.Name)
.ThenBy(x => x.Id)
.Select(staff => staff.Status == StaffStatus.Resigned
? CreateOffWeekShifts()
: sourceScheduleMap.GetValueOrDefault(staff.Id) ?? CreateDefaultWeekByRole(staff.RoleType, sourceTemplate))
: sourceScheduleMap.GetValueOrDefault(staff.Id) ?? [])
.ToList();
var targetTemplates = await dbContext.StoreStaffTemplates
@@ -526,9 +541,9 @@ public sealed class StoreStaffController(
? CreateOffWeekShifts()
: sourceScheduleSequence.Count > 0
? sourceScheduleSequence[index % sourceScheduleSequence.Count]
: CreateDefaultWeekByRole(staff.RoleType, sourceTemplate);
: [];
entities.AddRange(ToWeeklyEntities(targetStoreId, staff.Id, shifts));
entities.AddRange(ToWeeklyEntities(targetStoreId, staff.Id, shifts, sourceTemplate));
}
}
@@ -602,7 +617,7 @@ public sealed class StoreStaffController(
return normalized;
}
private async Task<StoreShiftTemplatesDto> GetOrCreateTemplateDtoAsync(long tenantId, long storeId, CancellationToken cancellationToken)
private async Task<StoreShiftTemplatesDto?> GetTemplateDtoAsync(long tenantId, long storeId, CancellationToken cancellationToken)
{
var entity = await dbContext.StoreStaffTemplates
.AsNoTracking()
@@ -610,7 +625,7 @@ public sealed class StoreStaffController(
if (entity is null)
{
return CreateDefaultTemplate();
return null;
}
return new StoreShiftTemplatesDto
@@ -633,83 +648,44 @@ public sealed class StoreStaffController(
};
}
private async Task EnsureStaffScheduleAsync(
long tenantId,
long storeId,
MerchantStaff staff,
StoreShiftTemplatesDto template,
CancellationToken cancellationToken)
{
var existingRows = await dbContext.StoreStaffWeeklySchedules
.Where(x => x.TenantId == tenantId && x.StoreId == storeId && x.StaffId == staff.Id)
.ToListAsync(cancellationToken);
if (existingRows.Count == 0)
{
var initialShifts = staff.Status == StaffStatus.Resigned
? CreateOffWeekShifts()
: CreateDefaultWeekByRole(staff.RoleType, template);
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
ToWeeklyEntities(storeId, staff.Id, initialShifts),
cancellationToken);
}
else if (staff.Status == StaffStatus.Resigned)
{
dbContext.StoreStaffWeeklySchedules.RemoveRange(existingRows);
await dbContext.StoreStaffWeeklySchedules.AddRangeAsync(
ToWeeklyEntities(storeId, staff.Id, CreateOffWeekShifts()),
cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
private static StoreShiftTemplatesDto NormalizeTemplate(StoreShiftTemplatesDto source)
{
var fallback = CreateDefaultTemplate();
source ??= new StoreShiftTemplatesDto();
return new StoreShiftTemplatesDto
{
Morning = new ShiftTemplateItemDto
{
StartTime = NormalizeTime(source.Morning.StartTime, fallback.Morning.StartTime),
EndTime = NormalizeTime(source.Morning.EndTime, fallback.Morning.EndTime)
StartTime = NormalizeTime(source.Morning.StartTime, string.Empty),
EndTime = NormalizeTime(source.Morning.EndTime, string.Empty)
},
Evening = new ShiftTemplateItemDto
{
StartTime = NormalizeTime(source.Evening.StartTime, fallback.Evening.StartTime),
EndTime = NormalizeTime(source.Evening.EndTime, fallback.Evening.EndTime)
StartTime = NormalizeTime(source.Evening.StartTime, string.Empty),
EndTime = NormalizeTime(source.Evening.EndTime, string.Empty)
},
Full = new ShiftTemplateItemDto
{
StartTime = NormalizeTime(source.Full.StartTime, fallback.Full.StartTime),
EndTime = NormalizeTime(source.Full.EndTime, fallback.Full.EndTime)
StartTime = NormalizeTime(source.Full.StartTime, string.Empty),
EndTime = NormalizeTime(source.Full.EndTime, string.Empty)
}
};
}
private static List<StaffDayShiftDto> NormalizeRowsToShifts(
List<StoreStaffWeeklySchedule> rows,
MerchantStaff? staff,
StoreShiftTemplatesDto template)
{
var fallback = staff is null ? CreateOffWeekShifts() : CreateDefaultWeekByRole(staff.RoleType, template);
var rowMap = rows.ToDictionary(x => x.DayOfWeek, x => x);
var results = new List<StaffDayShiftDto>();
for (var day = 0; day < 7; day++)
foreach (var row in rows
.Where(x => x.DayOfWeek is >= 0 and <= 6)
.OrderBy(x => x.DayOfWeek))
{
if (!rowMap.TryGetValue(day, out var row))
{
results.Add(fallback[day]);
continue;
}
var shiftType = StoreApiHelpers.ToShiftTypeText(row.ShiftType);
if (row.ShiftType == StoreStaffShiftType.Off)
{
results.Add(new StaffDayShiftDto
{
DayOfWeek = day,
DayOfWeek = row.DayOfWeek,
ShiftType = shiftType,
StartTime = string.Empty,
EndTime = string.Empty
@@ -720,7 +696,7 @@ public sealed class StoreStaffController(
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(row.ShiftType, template);
results.Add(new StaffDayShiftDto
{
DayOfWeek = day,
DayOfWeek = row.DayOfWeek,
ShiftType = shiftType,
StartTime = StoreApiHelpers.ToHHmm(row.StartTime ?? defaultStart ?? TimeSpan.Zero),
EndTime = StoreApiHelpers.ToHHmm(row.EndTime ?? defaultEnd ?? TimeSpan.Zero)
@@ -743,16 +719,11 @@ public sealed class StoreStaffController(
var normalized = new List<StaffDayShiftDto>();
for (var day = 0; day < 7; day++)
{
var fallbackShift = fallback.FirstOrDefault(x => x.DayOfWeek == day) ?? new StaffDayShiftDto
{
DayOfWeek = day,
ShiftType = "off",
StartTime = string.Empty,
EndTime = string.Empty
};
var fallbackShift = fallback.FirstOrDefault(x => x.DayOfWeek == day);
if (!inputMap.TryGetValue(day, out var input))
{
if (fallbackShift is null) continue;
normalized.Add(new StaffDayShiftDto
{
DayOfWeek = day,
@@ -781,8 +752,16 @@ public sealed class StoreStaffController(
{
DayOfWeek = day,
ShiftType = StoreApiHelpers.ToShiftTypeText(shiftType),
StartTime = NormalizeTime(input.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : fallbackShift.StartTime),
EndTime = NormalizeTime(input.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : fallbackShift.EndTime)
StartTime = NormalizeTime(
input.StartTime,
defaultStart.HasValue
? StoreApiHelpers.ToHHmm(defaultStart.Value)
: fallbackShift?.StartTime ?? string.Empty),
EndTime = NormalizeTime(
input.EndTime,
defaultEnd.HasValue
? StoreApiHelpers.ToHHmm(defaultEnd.Value)
: fallbackShift?.EndTime ?? string.Empty)
});
}
@@ -792,21 +771,22 @@ public sealed class StoreStaffController(
private static IEnumerable<StoreStaffWeeklySchedule> ToWeeklyEntities(
long storeId,
long staffId,
IEnumerable<StaffDayShiftDto> shifts)
IEnumerable<StaffDayShiftDto> shifts,
StoreShiftTemplatesDto template)
{
foreach (var shift in shifts)
{
var shiftType = StoreApiHelpers.ToShiftType(shift.ShiftType);
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, CreateDefaultTemplate());
var (defaultStart, defaultEnd) = ResolveShiftTimeRange(shiftType, template);
var startTime = shiftType == StoreStaffShiftType.Off
? null
: (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
NormalizeTime(shift.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : "09:00"),
NormalizeTime(shift.StartTime, defaultStart.HasValue ? StoreApiHelpers.ToHHmm(defaultStart.Value) : string.Empty),
"shift.startTime");
var endTime = shiftType == StoreStaffShiftType.Off
? null
: (TimeSpan?)StoreApiHelpers.ParseRequiredTime(
NormalizeTime(shift.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : "21:00"),
NormalizeTime(shift.EndTime, defaultEnd.HasValue ? StoreApiHelpers.ToHHmm(defaultEnd.Value) : string.Empty),
"shift.endTime");
yield return new StoreStaffWeeklySchedule
@@ -821,34 +801,6 @@ public sealed class StoreStaffController(
}
}
private static List<StaffDayShiftDto> CreateDefaultWeekByRole(StaffRoleType roleType, StoreShiftTemplatesDto template)
{
var pattern = roleType switch
{
StaffRoleType.Admin or StaffRoleType.Operator => new[] { "full", "full", "full", "full", "full", "morning", "off" },
StaffRoleType.FrontDesk => new[] { "morning", "morning", "off", "morning", "evening", "full", "full" },
StaffRoleType.Courier => new[] { "morning", "evening", "morning", "evening", "morning", "evening", "off" },
StaffRoleType.Kitchen => new[] { "full", "full", "evening", "off", "full", "full", "morning" },
_ => new[] { "morning", "morning", "off", "morning", "evening", "full", "full" }
};
var results = new List<StaffDayShiftDto>();
for (var day = 0; day < 7; day++)
{
var shiftType = StoreApiHelpers.ToShiftType(pattern[day]);
var (start, end) = ResolveShiftTimeRange(shiftType, template);
results.Add(new StaffDayShiftDto
{
DayOfWeek = day,
ShiftType = StoreApiHelpers.ToShiftTypeText(shiftType),
StartTime = start.HasValue ? StoreApiHelpers.ToHHmm(start.Value) : string.Empty,
EndTime = end.HasValue ? StoreApiHelpers.ToHHmm(end.Value) : string.Empty
});
}
return results;
}
private static List<StaffDayShiftDto> CreateOffWeekShifts()
{
return Enumerable.Range(0, 7).Select(day => new StaffDayShiftDto
@@ -883,26 +835,4 @@ public sealed class StoreStaffController(
? StoreApiHelpers.ToHHmm(parsed)
: fallback;
}
private static StoreShiftTemplatesDto CreateDefaultTemplate()
{
return new StoreShiftTemplatesDto
{
Morning = new ShiftTemplateItemDto
{
StartTime = "09:00",
EndTime = "14:00"
},
Evening = new ShiftTemplateItemDto
{
StartTime = "14:00",
EndTime = "21:00"
},
Full = new ShiftTemplateItemDto
{
StartTime = "09:00",
EndTime = "21:00"
}
};
}
}