實現上下班自動通知打卡
背景
由於最近上下班老是忘打卡,想要找一個能夠自動提醒上下班打卡的工具。由於 iPhone 和公司現有的工具都只能選擇固定時間提醒,而不能夠排除掉節假日,所以我自己開發了一個小工具,實現自動在企業微信推送上下班打卡訊息。
實現
首先描述一下需求,我只需要在工作日傳送上下班的通知,並且如果第二天是放假,應該早一點傳送通知,並且需要支援不同使用者配置不同的推送時間。
有了需求之後簡單描述一下實現方案,配置使用配置中心下發配置即可,首先需要獲取到節假日的資料資訊,可以從holiday-cn獲取到節假日和哪一天需要補班的資訊,我們這裡會定時拉取節假日資訊快取到本地,對外暴露一個介面給定時任務中心呼叫,會在呼叫的時候檢查當前時間是否需要進行通知,如果需要通知,會生成一個 task,透過時間輪延時執行。整體架構如下
時間資訊透過DateUtil來解析,時間輪透過 netty 提供的工具類來完成。netty 提供的工具類非常好用,簡易程式碼如下
HashedWheelTimer timer = new HashedWheelTimer();
// 延時5s執行
timer.newTimeout(timeout -> {
log.info("delay execute");
}, 5, TimeUnit.SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread(timer::stop));
task
會返回一個 timeout 物件,可以檢測任務是否已經過期,也可以手動取消任務。
基於以上的前提,我們簡要過一下程式碼,首先在程式啟動時,需要拉取節假日資訊到本地快取 (當然,也需要定時更新快取)
private void getHolidays() {
for (String holidayUrl : HOLIDAY_URL_LIST) {
String url = String.format(holidayUrl, getCurrentYear());
try (HttpResponse response = HttpUtil.createGet(url).timeout(5000).execute()) {
if (response.getStatus() != 200) {
log.error("can't find latest holiday, pls retry..");
continue;
}
String resp = response.body();
Object days = JSONPath.read(resp, "$.days");
List<Map<String, Object>> dayList = JSON.parseObject(JSON.toJSONString(days),
new TypeReference<List<Map<String, Object>>>() {
});
holidays.clear();
offDays.clear();
dayList.forEach(day -> {
if ((boolean) day.get("isOffDay")) {
holidays.add(String.valueOf(day.get("date")));
} else {
offDays.add(String.valueOf(day.get("date")));
}
});
return;
} catch (Throwable t) {
log.error("request {} err, e: ", url, t);
}
}
}
這裡我們需要區分節假日和補班日 2 個快取時間,節假日不需要傳送通知,而補班日需要傳送通知
String today = DateUtil.today();
// 非節假日和補班日才需要傳送通知
if ((isWeekDay() && !holidays.contains(today)) ||
(!isWeekDay() && offDays.contains(today))) {
Map<String, PunchConfigVO.PunchTime> persons = ConfigCenterUtil.PUNCH.getPersons();
Set<String> customPersons = new HashSet<>();
persons.forEach((empNo, punchTime) -> {
customPersons.add(empNo);
calculateTask(empNo, punchTime);
});
PunchConfigVO.DefaultConfig defaultConfig = ConfigCenterUtil.PUNCH.getDefaultConfig();
defaultConfig.getPersons().removeAll(customPersons);
for (String person : defaultConfig.getPersons()) {
calculateTask(person, new PunchConfigVO.PunchTime(defaultConfig.getClockIn(),
defaultConfig.getClockOut(), defaultConfig.getBeforeWeekendClockOut(),
defaultConfig.getAlertTamp()));
}
}
計算是否需要傳送通知,以及什麼是否傳送通知的核心邏輯如下
private void calculateTask(String empNo, PunchConfigVO.PunchTime punchTime) {
Set<String> targetAlertStamp = todayTasks.get(empNo);
// 如果當天沒有上班通知,才需要加上班通知的task
if (targetAlertStamp == null || !targetAlertStamp.contains(punchTime.getClockIn())) {
// 計算還剩多少時間還需要傳送通知
long clockInStamp = getSecUntilTarget(punchTime.getClockIn());
if (clockInStamp > 0) {
// 通知時間大於當前時間才需要通知
todayTasks.computeIfAbsent(empNo, k -> new ConcurrentSkipListSet<>())
.add(punchTime.getClockOut());
timeWheelManager.addTimer(new PunchTask(empNo, PunchType.CLOCK_IN,
punchTime.getClockIn()), clockInStamp);
}
// 上班前提前通知
long inTargetStamp = clockInStamp - punchTime.getAlertTamp();
if (inTargetStamp > 0) {
todayTasks.computeIfAbsent(empNo, k -> new ConcurrentSkipListSet<>())
.add(punchTime.getClockIn());
timeWheelManager.addTimer(
new PunchTask(empNo, PunchType.BEFORE_CLOCK_IN, getBeforeTargetStamp(
punchTime.getClockIn(), punchTime.getAlertTamp())),
inTargetStamp);
}
}
String nextDay = DateUtil.offsetDay(new Date(), 1).toDateStr();
if (holidays.contains(nextDay) || nextIsWeekend()) {
// 第二天放假時,提前通知
long beforeWeekendStamp = getSecUntilTarget(punchTime.getBeforeWeekendClockOut());
long outTargetStamp = beforeWeekendStamp - punchTime.getAlertTamp();
calculateClockOutTask(empNo, punchTime.getBeforeWeekendClockOut(), punchTime.getAlertTamp(), outTargetStamp);
} else {
if (targetAlertStamp == null || !targetAlertStamp.contains(punchTime.getClockOut())) {
long clockOutStamp = getSecUntilTarget(punchTime.getClockOut());
long outTargetStamp = clockOutStamp - punchTime.getAlertTamp();
calculateClockOutTask(empNo, punchTime.getClockOut(), punchTime.getAlertTamp(), outTargetStamp);
}
}
}
private void calculateClockOutTask(String empNo, String clockOut, int alertStamp, long clockOutStamp) {
if (clockOutStamp > 0) {
todayTasks.computeIfAbsent(empNo, k -> new ConcurrentSkipListSet<>())
.add(clockOut);
timeWheelManager.addTimer(
new PunchTask(empNo, PunchType.CLOCK_OUT, clockOut), clockOutStamp);
}
long outTargetStamp = clockOutStamp - alertStamp;
if (outTargetStamp > 0) {
todayTasks.computeIfAbsent(empNo, k -> new ConcurrentSkipListSet<>())
.add(clockOut);
timeWheelManager.addTimer(
new PunchTask(empNo, PunchType.BEFORE_CLOCK_OUT, getBeforeTargetStamp(
clockOut, alertStamp)),
outTargetStamp);
}
}
時間輪管理類負責新增和清理任務,如果當前使用者沒有對應的通知任務,直接新增 task;但是如果存在,並且任務沒有過期,就需要將快取中的 task 取消,然後將新的 task 重新新增進去,避免修改了任務執行之間之後重複傳送通知
public synchronized void addTimer(HolidayManager.PunchTask task, long after) {
TimerKey key = new TimerKey(task.getEmpNo(), task.getType());
TimeoutTask oldTask = timerTasks.get(key);
// 任務如果存在,需要清理掉已有的任務,再寫入新的任務
if (oldTask != null && !oldTask.getTimeout().isExpired() &&
!oldTask.getTargetStamp().equals(task.getTargetStamp())) {
oldTask.getTimeout().cancel();
Timeout timeout = timer.newTimeout(task, after, TimeUnit.SECONDS);
timerTasks.put(key, new TimeoutTask(timeout, task.getTargetStamp()));
log.info("task will be execute after {}s, empNo: {}", after, task.getEmpNo());
} else if (oldTask == null) {
// 任務不存在則直接新增
Timeout timeout = timer.newTimeout(task, after, TimeUnit.SECONDS);
timerTasks.put(key, new TimeoutTask(timeout, task.getTargetStamp()));
log.info("task will be execute after {}s, empNo: {}", after, task.getEmpNo());
}
}
定時通知效果如下
總結
以上就是上下班通知提醒的核心邏輯,整體核心程式碼不到 200 行,算是一個有趣的練手程式,當然還有很多最佳化場景沒有實現,比如保證多節點資料不重複執行,通知的儲存等,只有等後續如果有需求再慢慢最佳化啦。
相關文章
- Python實現企業微信上下班自動打卡程式Python
- Python實現企業微信自動打卡程式二:跳過節假日,隨機打卡時間,定時任務,失敗通知Python隨機
- 釘釘自動打卡
- Selenium原理、安裝與自動打卡實戰
- Spring AOP 實現《自動自動填充Entity》Spring
- Selenium自動化實現web自動化-1Web
- 用 Python 指令碼實現電腦喚醒後自動拍照 截圖併發郵件通知Python指令碼
- 簡易實現 HTTPS (一) 自動實現 sslHTTP
- IT 自動化:如何去實現
- Spring Boot中實現Thymeleaf通知Spring Boot
- MySQL空間函式實現位置打卡MySql函式
- 使用Jenkins實現前端自動化釋出和通知,讓你的釋出只需要git pushJenkins前端Git
- PropertyChanged.Fody自動通知屬性外掛
- 使用gulp實現前端自動化前端
- iOS如何實現自動化打包iOS
- 使用 fastlane 實現自動化打包AST
- 使用LangChain實現自動寫作LangChain
- 實現指令碼自動部署docker指令碼Docker
- Docker 搭建 Jenkins 實現自動部署DockerJenkins
- Vue實現自動觸發功能Vue
- 如何實現辦公自動化?
- Lock物件Condition介面實現等待/通知物件
- Yearning外接工單通知實現思路
- 是否只有實現了容器化、自動編排等等才算是實現了運維自動化?運維
- Android TextSwitcher通知公告自動上下滾動且帶點選事件Android事件
- [原] php + Laravel 實現部署自動化PHPLaravel
- 如何實現高度自動化測試?
- CRD實現自動化容器安全方法
- python自動化審計及實現Python
- GitLab整合kubernetes實現自動釋出Gitlab
- 搭建jenkins配合gitee實現自動部署JenkinsGitee
- python實現自動化辦公01Python
- 如何實現自己的SpringBoot自動配置Spring Boot
- sqlalchemy實現時間列自動更新SQL
- Selenium使用Cookie實現自動登入Cookie
- python實現自動搶課指令碼Python指令碼
- 利用Github Actions實現自動化部署Github
- Python 實現斷網自動重連Python