薪资系统中按特殊班次拆分工时记录的优化方案咨询
排班工时拆分的性能优化方案
问题背景
现有薪资应用基于以下两个类实现排班工时管理:
public class PayrollLineitems { public int PayrollLineItemID {get;set;} // 示例:1 public int CaregiverID {get;set;} // 示例:1 public DateTime ScheduleDate {get;set;} // 示例:2022-01-01 public DateTime StartTime {get;set;} // 示例:2022-01-01 10:00 public DateTime EndTime {get;set;} // 示例:2022-01-01 18:00 } public class SpecialShifts { public TimeSpan Start { get; set; } // 示例:08:00 public TimeSpan End { get; set; } // 示例:17:00 public string Days { get;set; } // 示例:6,7 → 6=周六,7=周日 }
需求为:将临床医生的排班记录按特殊班次规则拆分,符合特殊班次时段的工时标记为Shift(额外加薪10美元),其余标记为Regular。现有遍历匹配方法在SpecialShifts记录增多时性能低下,需优化拆分逻辑。
优化方案
1. 预编译特殊班次规则(空间换时间)
将SpecialShifts的规则转换为按星期几索引的时段集合,避免每次处理排班记录时遍历所有特殊班次:
- 用
Dictionary<int, List<(TimeSpan Start, TimeSpan End)>>存储,key为星期几(对应需求中的1-7,注意.NET默认DayOfWeek中周日为0,需转换为7),value为该星期几对应的所有特殊时段。 - 初始化时一次性完成规则转换,同时处理跨天时段(如22:00到次日06:00),并对每个星期几的时段进行排序、合并重叠/连续区间,减少后续比对次数。
2. 高效时段交集匹配
针对单条排班记录的工时区间,结合预编译的规则,通过以下方式快速匹配特殊时段:
- 直接根据排班日期的星期几取出对应的特殊时段集合,无需遍历所有
SpecialShifts。 - 对每个特殊时段,计算与当前工时区间的交集,拆分出
Shift和Regular时段,处理完交集后更新当前工时的起始点,直到剩余时段处理完毕。
3. 策略模式封装逻辑
将拆分逻辑封装为策略类,便于后续规则扩展:
- 定义
IPayrollSplitStrategy接口,包含Split(PayrollLineitem item)方法,返回拆分后的工时条目(含类型和时段)。 - 实现
SpecialShiftSplitStrategy类,内部集成预编译规则和交集匹配逻辑。 - 核心业务仅需调用策略的
Split方法,无需关心具体实现细节,后续新增规则只需添加新策略类即可。
代码示例
预编译特殊班次规则
// 预编译特殊班次规则到字典 var specialShiftMap = new Dictionary<int, List<(TimeSpan Start, TimeSpan End)>>(); foreach (var shift in specialShifts) { var dayNumbers = shift.Days.Split(',').Select(int.Parse).ToList(); foreach (var day in dayNumbers) { if (!specialShiftMap.ContainsKey(day)) { specialShiftMap[day] = new List<(TimeSpan, TimeSpan)>(); } // 处理跨天时段 if (shift.Start <= shift.End) { specialShiftMap[day].Add((shift.Start, shift.End)); } else { // 拆分为当天到23:59:59,以及次日00:00到结束 specialShiftMap[day].Add((shift.Start, TimeSpan.FromHours(23.9999))); var nextDay = day % 7 + 1; if (!specialShiftMap.ContainsKey(nextDay)) { specialShiftMap[nextDay] = new List<(TimeSpan, TimeSpan)>(); } specialShiftMap[nextDay].Add((TimeSpan.Zero, shift.End)); } } } // 对每个星期几的时段排序并合并重叠区间 foreach (var kvp in specialShiftMap) { var sortedShifts = kvp.Value.OrderBy(s => s.Start).ToList(); var mergedShifts = new List<(TimeSpan Start, TimeSpan End)>(); foreach (var shift in sortedShifts) { if (mergedShifts.Count == 0) { mergedShifts.Add(shift); } else { var last = mergedShifts.Last(); if (shift.Start <= last.End) { // 合并重叠/连续区间 mergedShifts.RemoveAt(mergedShifts.Count - 1); mergedShifts.Add((last.Start, TimeSpan.Max(last.End, shift.End))); } else { mergedShifts.Add(shift); } } } specialShiftMap[kvp.Key] = mergedShifts; }
单条排班记录拆分逻辑
public List<(DateTime Start, DateTime End, string Type)> SplitPayrollItem(PayrollLineitem item, Dictionary<int, List<(TimeSpan Start, TimeSpan End)>> specialShiftMap) { var result = new List<(DateTime, DateTime, string)>(); var scheduleDate = item.ScheduleDate.Date; // 转换.NET DayOfWeek为需求的星期几(周日→7,其余直接转int) var mappedDay = (int)scheduleDate.DayOfWeek == 0 ? 7 : (int)scheduleDate.DayOfWeek; var currentStart = item.StartTime; var currentEnd = item.EndTime; // 无对应星期几的特殊班次,直接标记为Regular if (!specialShiftMap.TryGetValue(mappedDay, out var specialShifts)) { result.Add((currentStart, currentEnd, "Regular")); return result; } foreach (var shift in specialShifts) { var shiftStart = scheduleDate.Add(shift.Start); var shiftEnd = scheduleDate.Add(shift.End); // 计算交集时段 var overlapStart = DateTime.Max(currentStart, shiftStart); var overlapEnd = DateTime.Min(currentEnd, shiftEnd); if (overlapStart < overlapEnd) { // 添加交集前的Regular时段 if (currentStart < overlapStart) { result.Add((currentStart, overlapStart, "Regular")); } // 添加Shift时段 result.Add((overlapStart, overlapEnd, "Shift")); // 更新当前起始点 currentStart = overlapEnd; if (currentStart >= currentEnd) { break; } } } // 添加剩余的Regular时段 if (currentStart < currentEnd) { result.Add((currentStart, currentEnd, "Regular")); } return result; }
内容的提问来源于stack exchange,提问作者Chethan SR
相关产品推荐
相关产品推荐

