// BatchUpdateEmployeeStatusBasedOnLeaveDate 批量更新员工状态
// 根据离职日期,将两个月前已离职但状态仍为正常的员工状态更新为离职
func (spSvc *StaffPayrollService) BatchUpdateEmployeeStatusBasedOnLeaveDate() (int, error) {
// 1. 查询所有有离职日期且状态正常的员工
query := `
SELECT employeeid, leave_date, status
FROM employee
WHERE leave_date != ''
AND status = '0'
`
type EmployeeInfo struct {
EmployeeID string `db:"employeeid"`
LeaveDate string `db:"leave_date"`
Status string `db:"status"`
}
var employees []EmployeeInfo
err := spSvc.db.Select(&employees, query)
if err != nil {
return 0, fmt.Errorf("failed to query employees: %w", err)
}
if len(employees) == 0 {
return 0, nil // 没有需要检查的员工
}
// 2. 过滤出两个月前离职的员工
// twoMonthsAgo := time.Now().AddDate(0, -2, 0)
var employeesToUpdate []EmployeeInfo
now := time.Now()
target := now.AddDate(0, -2, 0)
targetValue := target.Year()*100 + int(target.Month())
for _, emp := range employees {
// 解析 mm-dd-yy 格式的日期
t, err := time.Parse("01-02-06", emp.LeaveDate)
if err != nil {
return 0, fmt.Errorf("解析 %s 失败: %w", emp.LeaveDate, err)
}
value := t.Year()*100 + int(t.Month())
if value <= targetValue {
employeesToUpdate = append(employeesToUpdate, emp)
}
}
if len(employeesToUpdate) == 0 {
return 0, nil // 没有需要更新的员工
}
// 3. 批量更新状态
employeeIDs := lo.Map(employeesToUpdate, func(emp EmployeeInfo, _ int) string {
return emp.EmployeeID
})
sql, params, err := spSvc.dialect.Update("employee").
Where(goqu.Ex{"employeeid": employeeIDs}).
Set(goqu.Record{
"status": "1",
"updated_at": goqu.L("now()"),
}).ToSQL()
if err != nil {
return 0, err
}
// spSvc.logger.Debug("update status sql ", sql)
_, err = spSvc.db.Exec(sql, params...)
if err != nil {
return 0, err
}
return len(employeeIDs), err
}
- 本质:把「(年, 月)」二元组压缩成保序的整数
比较的目标是「离职日期所在月份 ≤ 两个月前所在月份」,粒度只到月。年×100+月 把 (2026, 6) 变成整数 202606,从而可以用一次 <= 完成比较:

这个映射是严格保序的:(y1,m1) < (y2,m2) ⇔ y1*100+m1 < y2*100+m2。
- 为什么基数必须是 100(而不是 10)
关键在于 100 > 12(一年最多 12 个月),保证「年」和「月」的位权不重叠。如果换成 ×10 就错了:
2025年12月 → 2025×10 + 12 = 20262
2026年01月 → 2026×10 + 1 = 20261
20262 > 20261 ❌ 时间上 2026-01 反而比 2025-12 "小"了,排序被破坏
任何基数 ≥ 13(如 年×12+月、年×100+月)都保序;选 100 是为了可读性------202606 一眼能读出"2026 年 06 月"。
- 为什么不直接比较字符串 leave_date
存储格式是 MM-DD-YY(如 06-30-26),这种格式不是字典序即时间序:
"01-02-06"(2006-01-02)vs "12-31-99"(1999-12-31):字符串比较 "01..." < "12...",会误判 2006 年早于 1999 年
所以必须先 time.Parse 解析出真实年月,再比较
- 为什么不用 time.Time.Before() 直接比较
原代码只想比较到「月」粒度。若用 t.Before(target):
time.Time 精确到时分秒,target = now.AddDate(0,-2,0) 带当天时刻,t 是解析出的 00:00:00
边界会不一致:今天 8 月 25 日 → target = 6 月 25 日 15:00。离职 6 月 30 日:原逻辑 202606 ≤ 202606 → 处理;Before 比较 6-30 00:00 < 6-25 15:00 → 不处理
要做到等价,得把 target 规范化到「目标月 1 号」再取下月边界,反而更绕
整数压缩写法简单、无时区干扰、边界语义清晰(整月粒度)。
5、其他对比方法
方法 1:直接比较 Year/Month(展开写)
if t.Year() < target.Year() ||
(t.Year() == target.Year() && t.Month() <= target.Month()) {
employeesToUpdate = append(employeesToUpdate, emp)
}
可读性最好,但代码长。语义与原逻辑完全等价(整数压缩就是它的简写)。
方法 2:格式化为 YYYY-MM 定宽字符串再比较
yearMonth := fmt.Sprintf("%04d-%02d", t.Year(), int(t.Month()))
targetYearMonth := fmt.Sprintf("%04d-%02d", target.Year(), int(target.Month()))
if yearMonth <= targetYearMonth { ... }
定宽 + 左对齐 + 左数字右补齐,字符串字典序与整数序等价。可读性好,但多一次格式化开销。
方法 3:年×12+月(数学上最小基数)
monthIndex := t.Year()*12 + int(t.Month()) - 1 // 或 t.Year()*12 + int(t.Month())
与 ×100 等价保序,但数字不直观(2026-06 → 24318)。
方法 4:把过滤下沉到 SQL(推荐,避免全表拉到 Go 里)
MySQL 有专门的 PERIOD_DIFF 周期函数,一条语句完成,连 Go 端解析都不需要:
UPDATE employee
SET status = '1', updated_at = NOW()
WHERE leave_date != ''
AND status = '0'
AND PERIOD_DIFF(
DATE_FORMAT(NOW(), '%Y%m'),
DATE_FORMAT(STR_TO_DATE(leave_date, '%m-%d-%y'), '%Y%m')
) >= 2;
或更直观地用日期减法:
AND STR_TO_DATE(leave_date, '%m-%d-%y') <= DATE_SUB(CURDATE(), INTERVAL 2 MONTH)
注意:STR_TO_DATE 的 %y 两位年转换规则与 Go 不同(MySQL 对 %y 的规则是 00-69 → 2000-2069,70-99 → 1970-1999),若数据里含 2069-2099 年的日期会解析偏差,需确认数据范围。全 SQL 化后还能顺手 SELECT COUNT(*) 拿影响行数。
方法 5:封装 YearMonth 类型(语义化,适合多处复用)
type YearMonth struct{ Year, Month int }
func (ym YearMonth) Less(other YearMonth) bool {
return ym.Year < other.Year || (ym.Year == other.Year && ym.Month < other.Month)
}