增加ai办公,考勤管理

This commit is contained in:
hero920103 committed 2026-11-14 18:15:55 +08:00
1 parent eb9b7beff5
commit 31ae07175d
46 files changed
+9250 -296

No files matched your search

@@ -0,0 +1,165 @@
package controllers
import (
"testing"
"time"
"server/models"
)
// TestOaAttendPeriodRange 月 / 季 / 年区间换算(含闰年 2 月)。
func TestOaAttendPeriodRange(t *testing.T) {
cases := []struct {
period, anchor, start, end string
}{
{"month", "2026-11-14", "2026-11-01", "2026-11-30"},
{"month", "2024-02-10", "2024-02-01", "2024-02-29"},
{"quarter", "2026-11-14", "2026-10-01", "2026-12-31"},
{"quarter", "2026-01-05", "2026-01-01", "2026-03-31"},
{"year", "2026-07-01", "2026-01-01", "2026-12-31"},
}
for _, item := range cases {
start, end, label, ok := oaAttendPeriodRange(item.period, item.anchor)
if !ok {
t.Fatalf("%s %s 解析失败", item.period, item.anchor)
}
if start != item.start || end != item.end {
t.Errorf("%s %s 期望 %s~%s,得到 %s~%s", item.period, item.anchor,
item.start, item.end, start, end)
}
if label == "" {
t.Errorf("%s %s 缺少周期标题", item.period, item.anchor)
}
}
if _, _, _, ok := oaAttendPeriodRange("month", "2026-13-01"); ok {
t.Error("非法日期应返回 ok=false")
}
}
// TestOaAttendAggregateCards 季度卡与年度卡由月度卡累加得出。
func TestOaAttendAggregateCards(t *testing.T) {
months := make([]oaAttendCard, 0, 12)
for month := 1; month <= 12; month++ {
card := oaAttendCard{Scheduled: 10, Matched: 8, Clocked: 9}
oaAttendFillCard(&card)
months = append(months, card)
}
quarters, yearCard := oaAttendAggregateCards(months, 2026)
if len(quarters) != 4 {
t.Fatalf("期望 4 个季度卡,得到 %d", len(quarters))
}
if quarters[0].Scheduled != 30 || quarters[0].Matched != 24 {
t.Errorf("Q1 期望 应出勤 30 / 匹配 24,得到 %d / %d", quarters[0].Scheduled, quarters[0].Matched)
}
if quarters[0].Missing != 6 || quarters[0].AttendanceRate != 80 {
t.Errorf("Q1 期望 缺卡 6 / 出勤率 80,得到 %d / %d", quarters[0].Missing, quarters[0].AttendanceRate)
}
if yearCard.Scheduled != 120 || yearCard.Matched != 96 || yearCard.AttendanceRate != 80 {
t.Errorf("年度卡累计异常:%+v", yearCard)
}
}
// TestOaShiftWindow 班次时间窗:普通班次与跨天班次都应换算成分钟区间。
func TestOaShiftWindow(t *testing.T) {
start, end, ok := oaShiftWindow("09:00", "18:00")
if !ok || start != 540 || end != 1080 {
t.Fatalf("09:00-18:00 期望 540-1080,得到 %d-%d ok=%v", start, end, ok)
}
start, end, ok = oaShiftWindow("22:00", "06:00")
if !ok || start != 1320 || end != 1800 {
t.Fatalf("22:00-06:00 期望 1320-1800,得到 %d-%d ok=%v", start, end, ok)
}
if _, _, ok := oaShiftWindow("", "18:00"); ok {
t.Error("缺少上班时间时应返回 ok=false")
}
}
// TestOaAttendClockInStatus 签到判定:正常 / 迟到 / 过早(不在时段)/ 无排班。
func TestOaAttendClockInStatus(t *testing.T) {
matched := &models.OaAttendance{
WorkStart: "09:00",
WorkEnd: "18:00",
MatchStatus: models.OaAttendMatchOK,
}
cases := []struct {
now string
status int8
}{
{"08:55", models.OaAttendClockNormal}, // 容差内早到
{"09:05", models.OaAttendClockNormal}, // 容差内晚到
{"09:30", models.OaAttendClockLate}, // 迟到
{"07:30", models.OaAttendClockOffTime}, // 提前超过 60 分钟
}
for _, item := range cases {
now, err := time.ParseInLocation("2006-01-02 15:04", "2026-11-14 "+item.now, time.Local)
if err != nil {
t.Fatalf("解析时间失败: %v", err)
}
status, _ := oaAttendClockInStatus(matched, now)
if status != item.status {
t.Errorf("%s 期望状态 %d,得到 %d", item.now, item.status, status)
}
}
// 未匹配排班:不阻断打卡,状态记为「无排班」
missing := &models.OaAttendance{MatchStatus: models.OaAttendMatchMissing}
now := time.Date(2026, 11, 14, 9, 0, 0, 0, time.Local)
if status, msg := oaAttendClockInStatus(missing, now); status != models.OaAttendClockNoShift || msg == "" {
t.Errorf("未匹配排班期望状态 %d 且有提醒文案,得到 %d %q", models.OaAttendClockNoShift, status, msg)
}
}
// TestOaAttendClockOutStatus 签退判定:早退与跨天班次凌晨签退。
func TestOaAttendClockOutStatus(t *testing.T) {
row := &models.OaAttendance{
WorkStart: "09:00",
WorkEnd: "18:00",
MatchStatus: models.OaAttendMatchOK,
}
early := time.Date(2026, 11, 14, 17, 0, 0, 0, time.Local)
if status, msg := oaAttendClockOutStatus(row, early); status != models.OaAttendClockLate || msg == "" {
t.Errorf("17:00 签退期望早退,得到 %d %q", status, msg)
}
night := &models.OaAttendance{
WorkStart: "22:00",
WorkEnd: "06:00",
MatchStatus: models.OaAttendMatchOK,
}
// 跨天班次:次日 06:10 签退应视为正常
nextMorning := time.Date(2026, 11, 15, 6, 10, 0, 0, time.Local)
if status, msg := oaAttendClockOutStatus(night, nextMorning); status != models.OaAttendClockNormal || msg != "" {
t.Errorf("06:10 跨天签退期望正常,得到 %d %q", status, msg)
}
}
// TestOaAttendDayResult 当日考勤结果口径:缺卡 > 无排班 > 迟到 > 早退 > 时段异常 > 未签退 > 正常。
func TestOaAttendDayResult(t *testing.T) {
if _, text := oaAttendDayResult("", 0, "", 0, models.OaAttendMatchMissing); text != "缺卡" {
t.Errorf("未签到应为缺卡,得到 %q", text)
}
if _, text := oaAttendDayResult("09:00", models.OaAttendClockOffTime, "18:00", models.OaAttendClockNormal, models.OaAttendMatchOK); text != "时段异常" {
t.Errorf("时段异常期望「时段异常」,得到 %q", text)
}
if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "18:00", models.OaAttendClockNormal, models.OaAttendMatchOK); text != "正常" {
t.Errorf("正常打卡期望「正常」,得到 %q", text)
}
if _, text := oaAttendDayResult("09:30", models.OaAttendClockLate, "", 0, models.OaAttendMatchOK); text != "迟到" {
t.Errorf("迟到期望「迟到」,得到 %q", text)
}
if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "17:00", models.OaAttendClockLate, models.OaAttendMatchOK); text != "早退" {
t.Errorf("早退期望「早退」,得到 %q", text)
}
if _, text := oaAttendDayResult("09:00", models.OaAttendClockNoShift, "18:00", models.OaAttendClockNoShift, models.OaAttendMatchMissing); text != "无排班" {
t.Errorf("无排班打卡期望「无排班」,得到 %q", text)
}
if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "", 0, models.OaAttendMatchOK); text != "未签退" {
t.Errorf("缺签退期望「未签退」,得到 %q", text)
}
}