工作时间支持自定义时段

设置页每日可选休息/全天/自定义时间范围(TimePicker),后端校验并规范化 HH:MM-HH:MM,补充跨午夜判定测试。
This commit is contained in:
yml2213
2026-07-17 21:39:26 +08:00
parent ff90d742ad
commit 7319799ec4
3 changed files with 360 additions and 53 deletions
+61 -31
View File
@@ -2,6 +2,7 @@ package handler
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -81,8 +82,61 @@ func parseWorkHours(raw string) map[string]string {
return result
}
// normalizeWorkHourSlot 规范化单日工作时间配置。
// 允许:空/休息(→ 空)、全天、HH:MM-HH:MM(支持跨午夜,如 22:00-06:00)。
func normalizeWorkHourSlot(val string) (string, error) {
val = strings.TrimSpace(val)
if val == "" || val == "休息" {
return "", nil
}
if val == "全天" {
return "全天", nil
}
// 仅允许一个区间:HH:MM-HH:MM
parts := strings.Split(val, "-")
if len(parts) != 2 {
return "", fmt.Errorf("工作时间格式无效,请使用 09:00-18:00 / 全天 / 空")
}
startS := strings.TrimSpace(parts[0])
endS := strings.TrimSpace(parts[1])
start, err1 := time.Parse("15:04", startS)
end, err2 := time.Parse("15:04", endS)
if err1 != nil || err2 != nil {
return "", fmt.Errorf("工作时间格式无效,请使用 09:00-18:0024 小时制)")
}
return start.Format("15:04") + "-" + end.Format("15:04"), nil
}
// slotCoversLocal 判断 slot(全天 / HH:MM-HH:MM)是否覆盖 local 时刻。
func slotCoversLocal(slot string, local time.Time, loc *time.Location) bool {
slot = strings.TrimSpace(slot)
if slot == "" || slot == "休息" {
return false
}
if slot == "全天" {
return true
}
parts := strings.Split(slot, "-")
if len(parts) != 2 {
// 非法配置 fail-open,避免因脏数据整站拒接
return true
}
start, err1 := time.ParseInLocation("15:04", strings.TrimSpace(parts[0]), loc)
end, err2 := time.ParseInLocation("15:04", strings.TrimSpace(parts[1]), loc)
if err1 != nil || err2 != nil {
return true
}
startAt := time.Date(local.Year(), local.Month(), local.Day(), start.Hour(), start.Minute(), 0, 0, loc)
endAt := time.Date(local.Year(), local.Month(), local.Day(), end.Hour(), end.Minute(), 0, 0, loc)
if !endAt.After(startAt) {
// 跨午夜:当前 >= start 或 < end
return !local.Before(startAt) || local.Before(endAt)
}
return !local.Before(startAt) && local.Before(endAt)
}
// isWithinWorkHours 根据租户工作时间配置判断当前是否在工作时段。
// 未配置或某日为空视为休息;时段格式 HH:MM-HH:MM。
// 未配置或某日为空视为休息;时段格式 HH:MM-HH:MM(可跨午夜)
func isWithinWorkHours(setting *model.TenantSetting, now time.Time) bool {
if setting == nil {
return true
@@ -110,30 +164,7 @@ func isWithinWorkHours(setting *model.TenantSetting, now time.Time) bool {
default:
key = "sunday"
}
slot := strings.TrimSpace(hours[key])
if slot == "" || slot == "休息" {
return false
}
if slot == "全天" {
return true
}
parts := strings.Split(slot, "-")
if len(parts) != 2 {
return true
}
start, err1 := time.ParseInLocation("15:04", strings.TrimSpace(parts[0]), loc)
end, err2 := time.ParseInLocation("15:04", strings.TrimSpace(parts[1]), loc)
if err1 != nil || err2 != nil {
return true
}
// 组合到当天日期
startAt := time.Date(local.Year(), local.Month(), local.Day(), start.Hour(), start.Minute(), 0, 0, loc)
endAt := time.Date(local.Year(), local.Month(), local.Day(), end.Hour(), end.Minute(), 0, 0, loc)
if !endAt.After(startAt) {
// 跨午夜:当前 >= start 或 < end
return !local.Before(startAt) || local.Before(endAt)
}
return !local.Before(startAt) && local.Before(endAt)
return slotCoversLocal(hours[key], local, loc)
}
func settingsResponse(setting *model.TenantSetting) gin.H {
@@ -271,13 +302,12 @@ func (h *SettingsHandler) Update(c *gin.Context) {
if req.WorkHours != nil {
normalized := map[string]string{}
for _, key := range weekdayKeys {
val := strings.TrimSpace(req.WorkHours[key])
if val == "" || val == "休息" || val == "全天" || strings.Contains(val, "-") {
normalized[key] = val
continue
slot, err := normalizeWorkHourSlot(req.WorkHours[key])
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "工作时间格式无效,请使用 09:00-18:00 / 全天 / 空"})
return
normalized[key] = slot
}
b, err := json.Marshal(normalized)
if err != nil {