202 lines
5.9 KiB
Go
202 lines
5.9 KiB
Go
// Package holiday 提供中国法定节假日/调休日数据。
|
|
//
|
|
// 数据获取优先级(从高到低):
|
|
// 1. 内存缓存;
|
|
// 2. 代码内置的 2024-2026 年数据(fallback.go,抄自国务院办公厅通知原文)——
|
|
// 命中即返回,不发起任何网络请求;
|
|
// 3. 数据库缓存(yz_backend_oa_holiday,由控制器读写);
|
|
// 4. 外网开源接口(holiday-cn,仅内置未收录的年份才会走到)。
|
|
//
|
|
// 把内置数据放在联网之前是刻意的性能设计:日历每次翻月都会请求,而跨境
|
|
// 请求的 TLS 握手就要 0.4~0.9 秒;内置数据与网络数据同源等价,直接用即可。
|
|
// 本包只处理"休假日"与"调休上班日"两类特殊日期,普通周末由前端按星期判定。
|
|
package holiday
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// 日期类型。只有"休假日"与"调休上班日"两类需要落库——
|
|
// 普通周末可以直接由星期几推算(周六日即周末),无需存储。
|
|
const (
|
|
// TypeWorkday 调休上班日(周末补班),前端显示「班」角标
|
|
TypeWorkday int8 = 0
|
|
// TypeHoliday 法定节假日放假,前端显示「休」角标
|
|
TypeHoliday int8 = 1
|
|
)
|
|
|
|
// 数据来源标记,写入数据库 source 列,便于排查某年份数据是哪来的。
|
|
const (
|
|
SourceHolidayCN = "holiday-cn" // 外网源:cdn(fastly).jsdelivr.net/gh/NateScarlet/holiday-cn
|
|
SourceBuiltin = "builtin" // 代码内置数据(2024-2026,官方通知抄录)
|
|
)
|
|
|
|
// Day 某一天节假日信息的统一内部表示,也是写入缓存与返回前端的结构。
|
|
type Day struct {
|
|
// Date "YYYY-MM-DD"
|
|
Date string `json:"date"`
|
|
// Name 节日名称,如「国庆节」「春节」;调休上班日固定为「调休上班」
|
|
Name string `json:"name"`
|
|
// Type 0 调休上班 / 1 法定放假;普通周末不入库、不返回
|
|
Type int8 `json:"type"`
|
|
// Year 所属年份
|
|
Year int `json:"year"`
|
|
// Wage 薪资倍数:1 普通,2 双倍,3 三倍(仅法定节假日当天为 3)
|
|
Wage int8 `json:"wage"`
|
|
// Source 数据来源标记
|
|
Source string `json:"source"`
|
|
}
|
|
|
|
// IsOffDay 是否休假日(法定放假)。注意普通周末不在本结构范围内,
|
|
// 调用方判断"这天是否休息"时必须把周末一起算上。
|
|
func (d Day) IsOffDay() bool {
|
|
return d.Type == TypeHoliday
|
|
}
|
|
|
|
// dateLayout 统一的日期字符串格式。
|
|
const dateLayout = "2006-01-02"
|
|
|
|
func parseDate(s string) (time.Time, error) {
|
|
return time.ParseInLocation(dateLayout, s, time.Local)
|
|
}
|
|
|
|
// normalizeDate 把可能带时分秒的时间串统一裁剪成 "YYYY-MM-DD"。
|
|
func normalizeDate(s string) string {
|
|
if len(s) > 10 {
|
|
return s[:10]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// expandRange 展开 [start, end] 闭区间为逐日列表;end 为空时视为仅 start 一天。
|
|
func expandRange(start, end string) []string {
|
|
if end == "" {
|
|
end = start
|
|
}
|
|
st, err := parseDate(normalizeDate(start))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
et, err := parseDate(normalizeDate(end))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, 16)
|
|
// 防御:区间异常(结束早于开始)或跨度过大时直接放弃,避免死循环
|
|
if et.Before(st) {
|
|
return nil
|
|
}
|
|
if et.Sub(st) > 60*24*time.Hour {
|
|
return nil
|
|
}
|
|
for d := st; !d.After(et); d = d.AddDate(0, 0, 1) {
|
|
out = append(out, d.Format(dateLayout))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// wageOfHolidayName 依据节日名与日期在假期中的位置推断倍数。
|
|
// 春节、国庆按"第几天"判断:前 3 天为三薪;其余节日首日为三薪。
|
|
func wageOfHolidayName(name string, indexInRange int) int8 {
|
|
switch name {
|
|
case "春节", "国庆节", "国庆节、中秋节":
|
|
if indexInRange < 3 {
|
|
return 3
|
|
}
|
|
return 2
|
|
default:
|
|
if indexInRange == 0 {
|
|
return 3
|
|
}
|
|
return 2
|
|
}
|
|
}
|
|
|
|
// Years 返回内置兜底数据覆盖的年份,用于告知调用方"网络与缓存都没有时
|
|
// 还能覆盖到哪一年",便于在接口响应里给出提示。
|
|
func Years() []int {
|
|
seen := make(map[int]bool, len(yearNotices))
|
|
out := make([]int, 0, len(yearNotices))
|
|
for _, n := range yearNotices {
|
|
if !seen[n.Year] {
|
|
seen[n.Year] = true
|
|
out = append(out, n.Year)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// builtinByYear 取某年份的内置数据;年份未收录时返回空切片。
|
|
func builtinByYear(year int) []Day {
|
|
out := make([]Day, 0, 32)
|
|
for _, d := range fallbackData {
|
|
if d.Year == year {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// annotateWage 依据同一节日的连续区间,为每天补上正确的薪资倍数。
|
|
// 输入需按日期升序排列;同名的连续日期视为同一次放假。
|
|
func annotateWage(list []Day) []Day {
|
|
if len(list) == 0 {
|
|
return list
|
|
}
|
|
prevName := ""
|
|
indexInRange := 0
|
|
for i := range list {
|
|
if list[i].Type != TypeHoliday {
|
|
// 调休上班日与普通日期不参与倍数计算,同时打断连续区间
|
|
prevName = ""
|
|
indexInRange = 0
|
|
list[i].Wage = 1
|
|
continue
|
|
}
|
|
if list[i].Name != prevName {
|
|
prevName = list[i].Name
|
|
indexInRange = 0
|
|
} else {
|
|
indexInRange++
|
|
}
|
|
list[i].Wage = wageOfHolidayName(list[i].Name, indexInRange)
|
|
}
|
|
return list
|
|
}
|
|
|
|
// dedupSort 按日期去重并升序排列;同日重复时保留先出现的记录。
|
|
func dedupSort(list []Day) []Day {
|
|
seen := make(map[string]int, len(list))
|
|
out := make([]Day, 0, len(list))
|
|
for _, d := range list {
|
|
if d.Date == "" {
|
|
continue
|
|
}
|
|
if idx, ok := seen[d.Date]; ok {
|
|
// 已有同日记录:休假日优先于调休上班日(同一日期不会既休又班,
|
|
// 但外网源偶有脏数据,这里做防御性取舍)
|
|
if d.Type == TypeHoliday && out[idx].Type == TypeWorkday {
|
|
out[idx] = d
|
|
}
|
|
continue
|
|
}
|
|
seen[d.Date] = len(out)
|
|
out = append(out, d)
|
|
}
|
|
sortByDate(out)
|
|
return out
|
|
}
|
|
|
|
func sortByDate(list []Day) {
|
|
// 数据量极小(单年最多几十条),直接插入排序,避免引入 sort 包的比较器开销
|
|
for i := 1; i < len(list); i++ {
|
|
cur := list[i]
|
|
j := i - 1
|
|
for j >= 0 && list[j].Date > cur.Date {
|
|
list[j+1] = list[j]
|
|
j--
|
|
}
|
|
list[j+1] = cur
|
|
}
|
|
}
|