优化排班功能

This commit is contained in:
hero920103 committed 2026-11-05 18:00:50 +08:00
1 parent 8acbe732ff
commit 8ba15d87b3
13 files changed
+2463 -565

No files matched your search

+156 -2
View File
@@ -8,6 +8,7 @@ import (
"time"
"server/models"
"server/pkg/holiday"
)
// scPatternDay 排班方案周期规则的每日快照。
@@ -212,6 +213,12 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
}
}
// 周期类方案:构造基准周期之外的顺延上下文(节假日 / 调休修正 + 星期节奏)
var rollExt *scRollExt
if plan.AlignMode == models.SchedTplAlignRoll || plan.AlignMode == models.SchedTplAlignWeek {
rollExt = scBuildRollExt(pattern, start, dates)
}
result := make(map[string]scPatternDay, len(dates))
workdays := make([]string, 0, len(dates))
for _, date := range dates {
@@ -219,7 +226,7 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
if !valid {
continue
}
rule, work := scRuleForDate(plan, rules, fallback, start, day, weekdays)
rule, work := scRuleForDate(plan, rules, fallback, start, day, weekdays, rollExt)
if !work {
continue
}
@@ -229,9 +236,109 @@ func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, date
return result, workdays, ""
}
// scRollExt 周期滚动方案(AlignMode=2,按月涂排)在基准周期之外的顺延上下文。
//
// 涂刷基准月里的节假日安排属于「基准月的特殊规则」,不应该跟着日序号平移到
// 后续月份——否则 10 月的国庆假期会错位落到 11 月的普通工作日上。周期之外的
// 推算规则:
// 1. 目标日期是法定节假日 → 休息;
// 2. 目标日期是调休上班日 → 上班(取涂刷里的班次);
// 3. 该星期在涂刷周期内呈现严格 休/班 交替(如大小周的周六)→ 按周序奇偶交替顺延;
// 4. 其余按涂刷月的星期节奏顺延(同星期几在周期内的多数状态)。
type scRollExt struct {
weekdayRules [8]scPatternDay // 下标 1-7(周一~周日):该星期的顺延规则
altRules [8]bool // 该星期是否隔周交替(严格 休/班 交替)
altEven [8]scPatternDay // 交替星期:周序为偶数时的规则
altOdd [8]scPatternDay // 交替星期:周序为奇数时的规则
defaultWork scPatternDay // 涂刷里的首个上班规则,供调休上班日使用
holidays map[string]int8 // 目标日期 → holiday.Type(0 调休上班 / 1 法定放假)
}
// scBuildRollExt 从涂刷周期构造顺延上下文。星期节奏取多数状态(work 数 - rest 数
// ≥ 0 视为上班,班次取该星期首个上班规则);若某星期在周期内严格 休/班 交替,
// 记录交替规则,顺延按周序奇偶保持交替(周序锚点 = start 所在周的周一)。
// 节假日数据按目标区间涉及的年份拉取,拉取失败只是少了节假日修正,不阻断发布。
func scBuildRollExt(pattern []scPatternDay, start time.Time, dates []string) *scRollExt {
ext := &scRollExt{holidays: map[string]int8{}}
workCount := [8]int{}
shiftBy := [8]scPatternDay{}
type weekSeqItem struct {
week int
work bool
item scPatternDay
}
byWeekday := [8][]weekSeqItem{}
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
for _, item := range pattern {
day := start.AddDate(0, 0, item.DayIndex)
w := scWeekdayOffset(day) + 1
if item.DayType == models.SchedTplDayWork {
workCount[w]++
if shiftBy[w].ShiftID == 0 {
shiftBy[w] = item
}
if ext.defaultWork.ShiftID == 0 {
ext.defaultWork = item
}
} else {
workCount[w]--
}
weekSeq := int(day.AddDate(0, 0, -scWeekdayOffset(day)).Sub(startMonday).Hours()/24) / 7
byWeekday[w] = append(byWeekday[w], weekSeqItem{week: weekSeq, work: item.DayType == models.SchedTplDayWork, item: item})
}
for w := 1; w <= 7; w++ {
if workCount[w] >= 0 && shiftBy[w].ShiftID != 0 {
ext.weekdayRules[w] = shiftBy[w]
} else {
ext.weekdayRules[w] = scPatternDay{DayIndex: w - 1, DayType: models.SchedTplDayRest}
}
// 隔周交替检测:该星期在涂刷周期内严格 休/班 交替(如大小周的周六,
// 一周上班一周休息),顺延时按周序奇偶保持交替,而不是取多数
list := byWeekday[w]
if len(list) < 2 {
continue
}
alternating := true
for i := 1; i < len(list); i++ {
if list[i].work == list[i-1].work {
alternating = false
break
}
}
if !alternating {
continue
}
ext.altRules[w] = true
if list[0].week%2 == 0 {
ext.altEven[w] = list[0].item
ext.altOdd[w] = list[1].item
} else {
ext.altOdd[w] = list[0].item
ext.altEven[w] = list[1].item
}
}
years := map[int]bool{}
for _, date := range dates {
if y := holidayYear(date); y > 0 {
years[y] = true
}
}
for y := range years {
items, _, err := holiday.FetchYear(y)
if err != nil {
continue
}
for _, d := range items {
ext.holidays[d.Date] = d.Type
}
}
return ext
}
// scRuleForDate 推算某一天命中的规则;第二个返回值表示当天是否上班。
// ext 是周期滚动方案(AlignMode=2)在基准周期之外的顺延上下文,可为 nil。
func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fallback *models.OaShift,
start, day time.Time, weekdays map[int]bool) (scPatternDay, bool) {
start, day time.Time, weekdays map[int]bool, ext *scRollExt) (scPatternDay, bool) {
switch plan.AlignMode {
case models.SchedTplAlignWeek:
weeks := plan.CycleDays / 7
@@ -241,6 +348,18 @@ func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fall
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
curMonday := day.AddDate(0, 0, -scWeekdayOffset(day))
diffWeeks := int(curMonday.Sub(startMonday).Hours()/24) / 7
// 首个周期内原样回放;周期之外目标月的节假日 / 调休优先修正
//(大小周等模板按自然周循环,周序永远交替不串位)
if ext != nil && diffWeeks >= weeks {
if hType, ok := ext.holidays[day.Format("2006-01-02")]; ok {
if hType == holiday.TypeHoliday {
return scPatternDay{}, false
}
if hType == holiday.TypeWorkday && ext.defaultWork.ShiftID != 0 {
return ext.defaultWork, true
}
}
}
weekIndex := scMod(diffWeeks+int(plan.WeekOffset), weeks)
return scRuleOfDay(rules, weekIndex*7+scWeekdayOffset(day))
@@ -250,6 +369,41 @@ func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fall
cycle = 1
}
diffDays := int(day.Sub(start).Hours() / 24)
// 基准周期内:按涂刷结果原样回放(涂刷月自己的节假日 / 周末已在保存时处理)
if diffDays < cycle || ext == nil {
return scRuleOfDay(rules, scMod(diffDays, cycle))
}
// 周期之外:先看目标日的节假日 / 调休特殊规则
if hType, ok := ext.holidays[day.Format("2006-01-02")]; ok {
if hType == holiday.TypeHoliday {
return scPatternDay{}, false
}
if hType == holiday.TypeWorkday && ext.defaultWork.ShiftID != 0 {
return ext.defaultWork, true
}
}
// 整月涂刷方案(周期=开始月天数):按星期节奏顺延,规避节假日 / 周末错位;
// 其他滚动周期(三班倒 / 做一休一等模板原生周期):保持相位纯滚动
daysInStartMonth := time.Date(start.Year(), start.Month()+1, 0, 0, 0, 0, 0, start.Location()).Day()
if cycle == daysInStartMonth {
w := scWeekdayOffset(day) + 1
rule := ext.weekdayRules[w]
// 隔周交替的星期(如大小周的周六):按周序奇偶保持交替
if ext.altRules[w] {
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
curMonday := day.AddDate(0, 0, -scWeekdayOffset(day))
weekSeq := int(curMonday.Sub(startMonday).Hours()/24) / 7
if weekSeq%2 == 0 {
rule = ext.altEven[w]
} else {
rule = ext.altOdd[w]
}
}
if rule.DayType != models.SchedTplDayWork {
return scPatternDay{}, false
}
return rule, true
}
return scRuleOfDay(rules, scMod(diffDays, cycle))
case models.SchedTplAlignMonth: