更新日历功能

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
+157
View File
@@ -11,6 +11,7 @@ import (
"unicode/utf8"
"server/models"
"server/pkg/holiday"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
@@ -240,6 +241,162 @@ func (c *BackendOaScheduleController) List() {
})
}
// Holidays GET /backend/oa/holiday
// 返回指定日期区间(或指定年份)内的中国法定节假日/调休日,供日历控件标注
// 「休 / 班」角标与节日名称。
//
// 参数(二选一):
// - start_date + end_date:"YYYY-MM-DD",日历按 42 格区间查询;
// - year:整年查询。
//
// 数据获取顺序:内存缓存 → 内置数据(2024-2026,零网络)→ 外网接口(仅未收录年份)。
// 已收录年份不会发起任何网络请求,因此日历翻月是纯内存操作、无卡顿。
// 响应里的 builtin_years 字段告知前端内置数据覆盖到哪一年。
func (c *BackendOaScheduleController) Holidays() {
if _, err := c.oaScheduleClaims(); err != nil {
c.oasJsonErr(401, 401, "未登录或无权限")
return
}
startDate := strings.TrimSpace(c.GetString("start_date"))
endDate := strings.TrimSpace(c.GetString("end_date"))
yearStr := strings.TrimSpace(c.GetString("year"))
// 参数归一化:区间优先,其次按年
var start, end string
if isValidOaDate(startDate) && isValidOaDate(endDate) && startDate <= endDate {
start, end = startDate, endDate
} else if y, err := strconv.Atoi(yearStr); err == nil && y >= 2000 && y <= 2100 {
start = fmt.Sprintf("%04d-01-01", y)
end = fmt.Sprintf("%04d-12-31", y)
} else {
// 未传参时默认当前年份,保证前端任何情况下都能拿到可用数据
now := time.Now()
start = now.Format("2006-01-01")
end = now.Format("2006") + "-12-31"
}
list := ensureOaHolidays(start, end)
c.oasOk(map[string]interface{}{
"list": list,
"start_date": start,
"end_date": end,
"builtin_years": holiday.Years(),
})
}
// ensureOaHolidays 取区间内的节假日数据。
//
// 顺序上的取舍(性能关键):先调 holiday.FetchYear,而不是先查数据库。
// 原因是 FetchYear 对已收录年份(2024-2026)直接返回内置数据、不发网络请求,
// 也无需查库;日历每次翻月都会走到这里,先查库会白白多一次 SQL 往返。
// 只有 FetchYear 真的联网拉到了数据(即年份未收录),才回写数据库缓存,
// 供后续同一年份的请求直接命中。
func ensureOaHolidays(start, end string) []holiday.Day {
list := make([]holiday.Day, 0, 64)
startYear, endYear := holidayYear(start), holidayYear(end)
for y := startYear; y <= endYear; y++ {
if y == 0 {
continue
}
items, src, ferr := holiday.FetchYear(y)
if ferr != nil || len(items) == 0 {
continue
}
// 仅当数据来自外网时才落库:内置数据本身就在代码里,落库没有收益,
// 反而会让每次翻月都产生一批无谓的写库操作。
if src == holiday.SourceHolidayCN {
saveOaHolidays(items)
}
for _, d := range items {
if d.Date >= start && d.Date <= end {
list = append(list, d)
}
}
}
return mergeOaHolidays(list)
}
// holidayYear 从 "YYYY-MM-DD" 取年份,失败返回 0。
func holidayYear(date string) int {
if len(date) < 4 {
return 0
}
y, err := strconv.Atoi(date[:4])
if err != nil {
return 0
}
return y
}
// saveOaHolidays 把外网拉取到的数据写入缓存表。
// 使用 insert-or-update:已存在的日期更新其名称/类型/来源,保证外网数据
// 修正后能覆盖库里的旧值。写入失败静默忽略(缓存非强依赖)。
func saveOaHolidays(list []holiday.Day) {
now := time.Now()
for _, d := range list {
if !isValidOaDate(d.Date) {
continue
}
var exist models.OaHoliday
ferr := models.Orm.QueryTable(new(models.OaHoliday)).
Filter("date", d.Date).
One(&exist)
if ferr == nil {
// 已存在:仅当来源或名称有变化时才更新,减少无谓写库
if exist.Name == d.Name && exist.Type == d.Type && exist.Source == d.Source {
continue
}
exist.Name = d.Name
exist.Type = d.Type
exist.Year = d.Year
exist.Wage = d.Wage
exist.Source = d.Source
exist.UpdateTime = &now
_, _ = models.Orm.Update(&exist, "Name", "Type", "Year", "Wage", "Source", "UpdateTime")
continue
}
item := models.OaHoliday{
Date: d.Date,
Name: d.Name,
Type: d.Type,
Year: d.Year,
Wage: d.Wage,
Source: d.Source,
CreateTime: now,
UpdateTime: &now,
}
_, _ = models.Orm.Insert(&item)
}
}
// mergeOaHolidays 按日期去重并升序排列;同日重复时保留先出现的记录
// (数据库缓存排在前面,因此缓存优先于外网新拉取的数据)。
func mergeOaHolidays(list []holiday.Day) []holiday.Day {
seen := make(map[string]bool, len(list))
out := make([]holiday.Day, 0, len(list))
for _, d := range list {
if d.Date == "" || seen[d.Date] {
continue
}
seen[d.Date] = true
out = append(out, d)
}
for i := 1; i < len(out); i++ {
cur := out[i]
j := i - 1
for j >= 0 && out[j].Date > cur.Date {
out[j+1] = out[j]
j--
}
out[j+1] = cur
}
return out
}
// Stats GET /backend/oa/schedule/stats
// 返回今日/本周/本月的日程统计与逾期数,供页面顶部统计卡片展示。
func (c *BackendOaScheduleController) Stats() {