售後工單系統常需計算「實際處理工時」,需精確扣除周六日與法定節假日。本文提供一個生產級工具類 GetWorkDay,支持毫秒級精度、多重載入參、自定義節假日列表、結果格式化輸出。
long)計算package com.utouu.uphone.commons;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
@Component
public class GetWorkDay {
/** 核心計算邏法:返回工作日毫秒數 */
private double getWorkdayTimeInMillis(long start, long end, List<Date> holidays) {
if (start > end) { long t = start; start = end; end = t; }
Calendar sCal = Calendar.getInstance(); sCal.setTimeInMillis(start);
Calendar eCal = Calendar.getInstance(); eCal.setTimeInMillis(end);
// 1. 扣除法定節假日
long holidayMillis = 0;
if (holidays != null) {
holidayMillis = getHolidaysInMillis(start, end, holidays);
holidays.clear();
}
// 2. 同周且非周末:直接返回差值
if (sameWeekNonWeekend(sCal, eCal)) {
return end - start - holidayMillis;
}
// 3. 同周且皆為周末:返回 0
if (sameWeekAllWeekend(sCal, eCal)) {
return 0;
}
// 4. 標準化起止時間(修正周末邊界)
long stdStart = validateStartTime(sCal);
long stdEnd = validateEndTime(eCal);
// 5. 計算兩個「下周一」之間的完整工作日毫秒數
Calendar sMon = getNextMonday((Calendar) sCal.clone());
Calendar eMon = getNextMonday((Calendar) eCal.clone());
int weeks = getDaysBetween(sMon, eMon) / 7;
double fullWeekMillis = weeks * 5.0 * 24 * 3600 * 1000;
// 6. 邊界偏移量
double boundaryMillis = calcBoundaryMillis(sCal, eCal, stdStart, stdEnd);
double result = fullWeekMillis + boundaryMillis - holidayMillis;
return result > 0 ? result : 0;
}
// -------- 輔助方法 --------
private long validateStartTime(Calendar cal) {
int dow = cal.get(Calendar.DAY_OF_WEEK);
if (dow == Calendar.SUNDAY) { // 周日 -> 下周一 09:00
cal.add(Calendar.DATE, 1);
return setToTime(cal, 9, 0, 0);
} else if (dow == Calendar.SATURDAY) { // 周六 -> 下周一 09:00
cal.add(Calendar.DATE, 2);
return setToTime(cal, 9, 0, 0);
}
return cal.getTimeInMillis();
}
private long validateEndTime(Calendar cal) {
int dow = cal.get(Calendar.DAY_OF_WEEK);
if (dow == Calendar.SUNDAY || dow == Calendar.SATURDAY) { // 周末 -> 本周五 18:00
cal.add(Calendar.DATE, dow == Calendar.SUNDAY ? -2 : -1);
return setToTime(cal, 18, 0, 0);
}
return cal.getTimeInMillis();
}
private long setToTime(Calendar cal, int hour, int min, int sec) {
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, min);
cal.set(Calendar.SECOND, sec);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTimeInMillis();
}
private Calendar getNextMonday(Calendar cal) {
int add = 9 - cal.get(Calendar.DAY_OF_WEEK); // 周日=1 -> +1, 周一=2 -> +7...
if (add == 8) add = 1;
cal.add(Calendar.DATE, add);
return cal;
}
private int getDaysBetween(Calendar start, Calendar end) {
if (start.after(end)) { Calendar t = start; start = end; end = t; }
int days = end.get(Calendar.DAY_OF_YEAR) - start.get(Calendar.DAY_OF_YEAR);
int y2 = end.get(Calendar.YEAR);
Calendar tmp = (Calendar) start.clone();
while (tmp.get(Calendar.YEAR) != y2) {
days += tmp.getActualMaximum(Calendar.DAY_OF_YEAR);
tmp.add(Calendar.YEAR, 1);
}
return days;
}
private long getHolidaysInMillis(long start, long end, List<Date> list) {
Calendar s = Calendar.getInstance(); s.setTimeInMillis(start);
Calendar e = Calendar.getInstance(); e.setTimeInMillis(end);
int count = 0;
for (Date h : list) {
Calendar c = Calendar.getInstance(); c.setTime(h);
if ((c.after(s) && c.before(e)) || c.getTimeInMillis() == s.getTimeInMillis() || c.getTimeInMillis() == e.getTimeInMillis()) {
count++;
}
}
return count * 24L * 3600 * 1000;
}
private double calcBoundaryMillis(Calendar sCal, Calendar eCal, long stdStart, long stdEnd) {
double startOffset = 0, endOffset = 0;
if (!isWeekend(sCal)) {
long todayMillis = sCal.getTimeInMillis();
long worked = todayMillis - stdStart; // 已工作毫秒
startOffset = (24*3600*1000) - worked; // 剩餘工時
}
if (!isWeekend(eCal)) {
long todayMillis = eCal.getTimeInMillis();
long worked = stdEnd - todayMillis; // 剩餘工時
endOffset = worked;
}
return startOffset - endOffset;
}
private boolean isWeekend(Calendar cal) {
int dow = cal.get(Calendar.DAY_OF_WEEK);
return dow == Calendar.SATURDAY || dow == Calendar.SUNDAY;
}
private boolean sameWeekNonWeekend(Calendar s, Calendar e) {
return s.get(Calendar.YEAR) == e.get(Calendar.YEAR) &&
s.get(Calendar.WEEK_OF_YEAR) == e.get(Calendar.WEEK_OF_YEAR) &&
!isWeekend(s) && !isWeekend(e);
}
private boolean sameWeekAllWeekend(Calendar s, Calendar e) {
return s.get(Calendar.YEAR) == e.get(Calendar.YEAR) &&
s.get(Calendar.WEEK_OF_YEAR) == e.get(Calendar.WEEK_OF_YEAR) &&
isWeekend(s) && isWeekend(e);
}
// -------- 公開重載方法 --------
public double getWorkdayTimeInMillisExcWeekend(long start, long end) {
return getWorkdayTimeInMillis(start, end, null);
}
public double getWorkdayTimeInMillisExcWeekendHolidays(String start, String end, String fmt, List<Date> holidays) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(fmt);
return getWorkdayTimeInMillis(sdf.parse(start).getTime(), sdf.parse(end).getTime(), holidays);
} catch (ParseException e) { return 0; }
}
public double getWorkdayTimeInMillis(Long s, Long e) { return getWorkdayTimeInMillis(s, e, null); }
public double getWorkdayTimeInMillis(Date s, Date e) { return getWorkdayTimeInMillis(s.getTime(), e.getTime(), null); }
public double getWorkdayTimeInMillis(String s, String e, String fmt) {
try { return getWorkdayTimeInMillis(new SimpleDateFormat(fmt).parse(s).getTime(), new SimpleDateFormat(fmt).parse(e).getTime(), null); }
catch (ParseException ex) { return 0; }
}
// -------- 格式化輸出 --------
public String formatDuring(double ms) {
long d = (long)(ms / 86400000);
long h = (long)((ms % 86400000) / 3600000);
long m = (long)((ms % 3600000) / 60000);
long s = (long)((ms % 60000) / 1000);
return String.format("%d days %d hours %d minutes %d seconds", d, h, m, s);
}
public double formateToDay(double ms) { return ms / 86400000; }
// -------- 2016 年節假日示例(實際項目建議從數據庫/配置中心讀取) --------
public List<Date> initHoliday() throws ParseException {
List<Date> list = new ArrayList<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] days = {
"2016-01-01","2016-01-02","2016-01-03", // 元旦
"2016-02-07","2016-02-08","2016-02-09","2016-02-10","2016-02-11","2016-02-12","2016-02-13", // 春節
"2016-04-02","2016-04-03","2016-04-04", // 清明
"2016-04-30","2016-05-01","2016-05-02", // 勞動節
"2016-06-09","2016-06-10","2016-06-11", // 端午
"2016-09-15","2016-09-16","2016-09-17", // 中秋
"2016-10-01","2016-10-02","2016-10-03","2016-10-04","2016-10-05","2016-10-06","2016-10-07" // 國慶
};
for (String d : days) list.add(sdf.parse(d));
return list;
}
}
@Autowired GetWorkDay workDay;
// 計算 2016-06-30 17:12:53 至 2016-08-30 11:27:50 的工作日
List<Date> holidays = workDay.initHoliday();
double ms = workDay.getWorkdayTimeInMillisExcWeekendHolidays(
"2016-06-30 17-12-53", "2016-08-30 11-27-50",
"yyyy-MM-dd HH-mm-ss", holidays);
System.out.println("工作日天數:" + workDay.formateToDay(ms)); // 如 42.3 天
System.out.println("詳細時長:" + workDay.formatDuring(ms)); // 如 42 days 3 hours ...
| 場景 | 處理邏輯 |
|---|---|
| 起始時間在周末 | 自動順延至下周一 09:00 |
| 結束時間在周末 | 自動前推至本周五 18:00 |
| 跨年計算 | getDaysBetween 通過 getActualMaximum(DAY_OF_YEAR) 正確處理閏年 |
| 節假日配置 | 建議改為從數據庫/配置中心動態加載,支持多年份 |
| 工作制假設 | 默認 09:00-18:00,可通過修改 setToTime 參數調整 |
java.time (JSR-310) 替代 Calendar,代碼更簡潔線程安全