更新日历功能

This commit is contained in:
2026-09-24 15:14:30 +08:00
parent c5ed596008
commit bc9e32d62a
13 changed files with 1446 additions and 6 deletions
+201
View File
@@ -0,0 +1,201 @@
// 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
}
}