更新若干事项

This commit is contained in:
hero920103 committed 2026-10-26 08:54:34 +08:00
1 parent 68cdaf38fa
commit 9898962836
30 files changed
+7715

No files matched your search

@@ -0,0 +1,353 @@
package controllers
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"server/models"
)
// scPatternDay 排班方案周期规则的每日快照。
//
// AlignMode=1/2/3 时,Plan.Pattern 保存该结构的 JSON 数组:模板的规则在
// 「调用模板」时复制进方案,之后平台模板怎么改都不影响已保存的方案。
// 发布方案时按自然周 / 开始日滚动 / 自然月推算每天命中哪一天规则。
type scPatternDay struct {
DayIndex int `json:"day_index"`
DayType int8 `json:"day_type"`
ShiftID uint64 `json:"shift_id"`
ShiftName string `json:"shift_name"`
ShortName string `json:"short_name"`
Color string `json:"color"`
WorkStart string `json:"work_start"`
WorkEnd string `json:"work_end"`
RestStart string `json:"rest_start"`
RestEnd string `json:"rest_end"`
WorkHours float64 `json:"work_hours"`
Remark string `json:"remark"`
}
// scPatternFromBody 解析前端提交的 pattern;兼容数组与 JSON 字符串两种形式。
func scPatternFromBody(body map[string]interface{}) []scPatternDay {
raw, ok := body["pattern"]
if !ok || raw == nil {
return nil
}
list := []scPatternDay{}
switch value := raw.(type) {
case string:
if strings.TrimSpace(value) == "" {
return nil
}
if err := json.Unmarshal([]byte(value), &list); err != nil {
return nil
}
case []interface{}:
buf, err := json.Marshal(value)
if err != nil {
return nil
}
if err := json.Unmarshal(buf, &list); err != nil {
return nil
}
default:
return nil
}
return list
}
func scPatternToJSON(days []scPatternDay) string {
if len(days) == 0 {
return ""
}
buf, err := json.Marshal(days)
if err != nil {
return ""
}
return string(buf)
}
func scPatternFromJSON(raw string) []scPatternDay {
days := []scPatternDay{}
if strings.TrimSpace(raw) == "" {
return days
}
if err := json.Unmarshal([]byte(raw), &days); err != nil {
return []scPatternDay{}
}
return days
}
// scValidatePattern 校验周期规则,返回补齐默认值并按 day_index 排序后的规则。
func scValidatePattern(days []scPatternDay, alignMode int8, cycleDays int) ([]scPatternDay, string) {
if alignMode < models.SchedTplAlignWeek || alignMode > models.SchedTplAlignMonth {
return nil, "排班方式不合法"
}
if len(days) == 0 {
return nil, "缺少排班规则,请重新选择模板或班次"
}
if alignMode == models.SchedTplAlignMonth {
// 月休按自然月计算,周期天数无意义
cycleDays = 0
} else {
if cycleDays < 2 || cycleDays > 31 {
return nil, "周期天数需在 2~31 天之间"
}
if alignMode == models.SchedTplAlignWeek && cycleDays%7 != 0 {
return nil, "按自然周排班时,周期天数必须是 7 的倍数(7 / 14 / 21 / 28)"
}
if len(days) != cycleDays {
return nil, fmt.Sprintf("周期为 %d 天,需要配置 %d 天的规则", cycleDays, cycleDays)
}
}
seen := map[int]bool{}
workCount := 0
result := make([]scPatternDay, 0, len(days))
for _, item := range days {
index := item.DayIndex
if alignMode == models.SchedTplAlignMonth {
index = len(result)
}
if index < 0 || (alignMode != models.SchedTplAlignMonth && index >= cycleDays) {
return nil, "规则的天序号不合法"
}
if seen[index] {
return nil, "规则的天序号重复"
}
seen[index] = true
if item.DayType != models.SchedTplDayWork && item.DayType != models.SchedTplDayRest {
return nil, "规则类型不合法(1=上班 2=休息)"
}
row := scPatternDay{DayIndex: index, DayType: item.DayType, Remark: strings.TrimSpace(item.Remark)}
if item.DayType == models.SchedTplDayWork {
workCount++
row.ShiftID = item.ShiftID
row.ShiftName = strings.TrimSpace(item.ShiftName)
row.ShortName = strings.TrimSpace(item.ShortName)
row.Color = strings.TrimSpace(item.Color)
row.WorkStart = strings.TrimSpace(item.WorkStart)
row.WorkEnd = strings.TrimSpace(item.WorkEnd)
row.RestStart = strings.TrimSpace(item.RestStart)
row.RestEnd = strings.TrimSpace(item.RestEnd)
row.WorkHours = item.WorkHours
if row.ShiftName == "" {
return nil, fmt.Sprintf("第 %d 天选择上班,班次不能为空", index+1)
}
if row.ShortName == "" {
row.ShortName = row.ShiftName
}
if row.Color == "" {
row.Color = "#409EFF"
}
if !isValidOaTime(row.WorkStart) || !isValidOaTime(row.WorkEnd) {
return nil, fmt.Sprintf("第 %d 天的上下班时间格式应为 HH:mm", index+1)
}
if row.WorkStart == row.WorkEnd {
return nil, fmt.Sprintf("第 %d 天的上下班时间不能相同", index+1)
}
if (row.RestStart == "") != (row.RestEnd == "") {
return nil, fmt.Sprintf("第 %d 天的休息开始与结束时间需要同时填写", index+1)
}
if row.RestStart != "" && (!isValidOaTime(row.RestStart) || !isValidOaTime(row.RestEnd)) {
return nil, fmt.Sprintf("第 %d 天的休息时间格式应为 HH:mm", index+1)
}
if row.WorkHours <= 0 {
row.WorkHours = scCalcWorkHours(row.WorkStart, row.WorkEnd, row.RestStart, row.RestEnd)
}
}
result = append(result, row)
}
if workCount == 0 {
return nil, "排班规则至少需要一天上班"
}
sort.Slice(result, func(i, j int) bool { return result[i].DayIndex < result[j].DayIndex })
return result, ""
}
// scPrimaryPatternShift 取规则里第一个上班日,用于方案列表展示班次名 / 颜色。
func scPrimaryPatternShift(days []scPatternDay) (scPatternDay, bool) {
for _, item := range days {
if item.DayType == models.SchedTplDayWork {
return item, true
}
}
return scPatternDay{}, false
}
// scPlanPatternDays 取方案中保存的周期规则。
func scPlanPatternDays(plan *models.OaSchedulePlan) []scPatternDay {
return scPatternFromJSON(plan.Pattern)
}
// scPlanWorkRules 计算方案在日期区间内「需要上班」的日期与对应的班次规则。
//
// fallback 是按星期模式(AlignMode=0)使用的固定班次;模板模式下可为 nil。
// 返回的 map 以日期字符串为键,workdays 为升序的上班日期列表。
func scPlanWorkRules(plan *models.OaSchedulePlan, fallback *models.OaShift, dates []string) (map[string]scPatternDay, []string, string) {
pattern := scPlanPatternDays(plan)
if len(pattern) == 0 && fallback == nil {
return nil, nil, "排班规则为空,请重新选择模板或班次"
}
start, ok := scParseDate(normalizeOaDate(plan.StartDate))
if !ok {
return nil, nil, "开始日期格式错误"
}
rules := make(map[int]scPatternDay, len(pattern))
for _, item := range pattern {
rules[item.DayIndex] = item
}
weekdays := map[int]bool{}
if strings.TrimSpace(plan.Weekdays) != "" {
list := []int{}
if err := json.Unmarshal([]byte(plan.Weekdays), &list); err == nil {
for _, day := range list {
weekdays[day] = true
}
}
}
result := make(map[string]scPatternDay, len(dates))
workdays := make([]string, 0, len(dates))
for _, date := range dates {
day, valid := scParseDate(date)
if !valid {
continue
}
rule, work := scRuleForDate(plan, rules, fallback, start, day, weekdays)
if !work {
continue
}
result[date] = rule
workdays = append(workdays, date)
}
return result, workdays, ""
}
// scRuleForDate 推算某一天命中的规则;第二个返回值表示当天是否上班。
func scRuleForDate(plan *models.OaSchedulePlan, rules map[int]scPatternDay, fallback *models.OaShift,
start, day time.Time, weekdays map[int]bool) (scPatternDay, bool) {
switch plan.AlignMode {
case models.SchedTplAlignWeek:
weeks := plan.CycleDays / 7
if weeks < 1 {
weeks = 1
}
startMonday := start.AddDate(0, 0, -scWeekdayOffset(start))
curMonday := day.AddDate(0, 0, -scWeekdayOffset(day))
diffWeeks := int(curMonday.Sub(startMonday).Hours()/24) / 7
weekIndex := scMod(diffWeeks+int(plan.WeekOffset), weeks)
return scRuleOfDay(rules, weekIndex*7+scWeekdayOffset(day))
case models.SchedTplAlignRoll:
cycle := plan.CycleDays
if cycle < 1 {
cycle = 1
}
diffDays := int(day.Sub(start).Hours() / 24)
return scRuleOfDay(rules, scMod(diffDays, cycle))
case models.SchedTplAlignMonth:
if scMonthRestDay(plan, day) {
return scPatternDay{}, false
}
index := 0
if rule, ok := rules[0]; ok && rule.DayType != models.SchedTplDayWork {
for _, item := range rules {
if item.DayType == models.SchedTplDayWork {
index = item.DayIndex
break
}
}
}
return scRuleOfDay(rules, index)
}
// 按星期模式(AlignMode=0):weekdays 为空表示每天都上班
weekday := int(day.Weekday())
if weekday == 0 {
weekday = 7
}
if len(weekdays) > 0 && !weekdays[weekday] {
return scPatternDay{}, false
}
if fallback == nil {
return scPatternDay{}, false
}
return scPatternDay{
DayType: models.SchedTplDayWork,
ShiftID: fallback.ID,
ShiftName: fallback.Name,
ShortName: fallback.ShortName,
Color: fallback.Color,
WorkStart: fallback.WorkStart,
WorkEnd: fallback.WorkEnd,
RestStart: fallback.RestStart,
RestEnd: fallback.RestEnd,
WorkHours: fallback.WorkHours,
}, true
}
func scRuleOfDay(rules map[int]scPatternDay, index int) (scPatternDay, bool) {
rule, ok := rules[index]
if !ok || rule.DayType != models.SchedTplDayWork {
return scPatternDay{}, false
}
return rule, true
}
// scWeekdayOffset 返回相对本周周一的天数(周一=0 ... 周日=6)。
func scWeekdayOffset(day time.Time) int {
return (int(day.Weekday()) + 6) % 7
}
func scMod(value, base int) int {
if base <= 0 {
return 0
}
result := value % base
if result < 0 {
result += base
}
return result
}
// scMonthRestDay 判断自然月排班中某天是否休息。
//
// 指定休息周几时,先取当月该周几的前 N 天;不足 N 天时从月末向前补齐。
// 未指定周几(0)时直接取月末最后 N 天。
func scMonthRestDay(plan *models.OaSchedulePlan, day time.Time) bool {
restDays := int(plan.MonthRestDays)
if restDays <= 0 {
return false
}
year, month := day.Year(), day.Month()
first := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
daysInMonth := first.AddDate(0, 1, -1).Day()
if restDays >= daysInMonth {
return true
}
restSet := map[int]bool{}
weekday := int(plan.MonthRestWeekday)
if weekday >= 1 && weekday <= 7 {
for d := 1; d <= daysInMonth && len(restSet) < restDays; d++ {
w := int(time.Date(year, month, d, 0, 0, 0, 0, time.Local).Weekday())
if w == 0 {
w = 7
}
if w == weekday {
restSet[d] = true
}
}
}
for d := daysInMonth; d >= 1 && len(restSet) < restDays; d-- {
restSet[d] = true
}
return restSet[day.Day()]
}