讓打卡小工具"智能一點”:添加請假過濾、Token自動刷新
大家好,我是楊成功。
前面寫了一篇文章,介紹了如何用 Node.js + 釘釘 API 實現考勤打卡連續提醒的小工具。
有的同學留言說為什么不直接調用釘釘 API 自動打卡(這個我也想過)。可惜我翻遍了釘釘的文檔都沒有找到這個 API。
再說了,怎么可能有這個 API 呢?想啥呢?
還有的同學嚴厲的指出了問題:“我請假了你還一直提醒?token 用幾個小時就過期!”。
針對這兩個問題,我們在上次實現代碼的基礎上進行優化,添加兩個邏輯:
- 獲取未打卡的人員時,過濾已請假人員。
- 當 token 過期時,自動刷新 token。
接下來我們一起實現新增的需求,優化打卡功能。
過濾已請假人員
使用釘釘 API 可以獲取一些人員的打卡狀態。
目前我們的做法是,將需要檢測打卡狀態的人員(我們全組人員)的 userid 維護在一個列表中,然后獲取到這些人的打卡數據,從而篩選出未打卡的人員。
特殊情況是,假設我們組的一個組員今天請假了,他會被當作未打卡人員不斷地被提醒,這是不合理的。
其實我們應該將已請假的人員排除在外。要實現這個,第一步是要獲取今日已請假的人員。
獲取請假狀態的 API 如下:
API 地址:${baseURL}/topapi/attendance/getleavestatus。
請求方法:POST。
這個 API 的請求體是一個對象,對象的屬性如下:
- userid_list:查詢請假狀態的 userid 列表。
- start_time:查詢開始時間(當天上班時間)。
- end_time:查詢結束時間(當天下班時間)。
- size:返回條數,最大 20。
- offset:分頁,從 0 開始。
將獲取請假狀態寫為一個單獨的方法,代碼如下:
const dayjs = require('dayjs');
const access_token = new DingToken().get();
// 獲取請假狀態
const getLeaveStatus = async (userid_list) => {
let params = {
access_token,
};
let body = {
start_time: dayjs().startOf('day').valueOf(),
end_time: dayjs().endOf('day').valueOf(),
userid_list: userid_list.join(), // userid 列表
offset: 0,
size: 20,
};
let res = await axios.post(`${baseURL}/topapi/attendance/getleavestatus`, body, { params });
if (res.errcode != 0) {
return res;
} else {
return res.result.leave_status.map((row) => row.userid);
}
};
執行以上方法后,就可以獲取到當天已請假的用戶。接著在所有需要檢測打卡狀態的用戶列表中,過濾掉已請假的用戶:
// 需要檢測打卡的 userid 數組
let alluids = ['xxx', 'xxxx'];
// 獲取請假狀態
let leaveRes = await getLeaveStatus(alluids);
if (leaveRes.errcode) {
return leaveRes;
}
alluids = alluids.filter((uid) => !leaveRes.includes(uid));
console.log(alluids); // 過濾后的 userid 數組
這樣就不會對已請假的用戶發出提醒了。
釘釘 token 自動刷新
在獲取釘釘 API 時,首先要獲取接口調用憑證(也就是 access_token),每個 API 調用時都要攜帶這個憑證。但這個憑證是有期限的,有效期一過 API 就會被禁止調用。
因此,這里非常重要的一個優化點,就是自動刷新 access_token。
怎么做呢?其實和在前端項目中實現一樣,在 axios 的攔截器中判斷 access_token 是否過期,如果過期則重新獲取,然后繼續執行請求。
首先,將獲取憑證寫成一個單獨的方法,如下:
const fetchToken = async () => {
try {
let params = {
appkey: 'xxx',
appsecret: 'xxx',
};
let url = 'https://oapi.dingtalk.com/gettoken';
let result = await axios.get(url, { params });
if (result.data.errcode != 0) {
throw result.data;
} else {
let token_str = JSON.stringify({
token: result.data.access_token,
expire: Date.now() + result.data.expires_in * 1000,
});
new DingToken().set(token_str);
return token_str;
}
} catch (error) {
console.log(error);
}
};
這個方法主要是調用獲取憑證的 API,調用成功后會返回 access_token 和有效時間。這里我們要設置一個過期時間,就是當前時間+有效時間,生成一個過期時間的時間戳:
Date.now() + result.data.expires_in * 1000,
這里還有一個 DingToken 類是用于獲取和存儲 access_token 的,代碼如下:
var fs = require('fs');
var catch_dir = path.resolve(__dirname, '../', 'catch');
class DingToken {
get() {
let res = fs.readFileSync(`${catch_dir}/ding_token.json`);
return res.toString() || null;
}
set(token) {
fs.writeFileSync(`${catch_dir}/ding_token.json`, token);
}
}
將 access_token 和過期時間組成一個 JSON 字符串存儲到文件中,接下來就可以在 axios 的請求攔截器中獲取到這個 JSON 數據,然后判斷當前時間是否大于過期時間。
如果是,則重新調用 fetchToken() 方法生成新 token,并繼續執行請求。攔截器代碼如下:
const axios = require('axios');
const instance = axios.create({
baseURL: 'https://oapi.dingtalk.com',
timeout: 5000,
});
const dingToken = new DingToken();
// 請求攔截器
instance.interceptors.request.use(async (config) => {
if (!config.params.access_token) {
let catoken = {};
if (dingToken.get()) {
catoken = JSON.parse(dingToken.get());
// 判斷是否過期
if (Date.now() - catoken.expire >= 0) {
console.log('釘釘 token 過期');
await fetchToken();
catoken = JSON.parse(dingToken.get());
}
} else {
// 第一次獲取token
await fetchToken();
catoken = JSON.parse(dingToken.get());
}
// 將 token 攜帶至請求頭
config.params.access_token = catoken.token;
}
return config;
});
通過上面在攔截器中編寫的邏輯,我們就不需要關心 access_token 過期了。并且我們是在 token 過期之后才會重新請求,因此也不會觸發調用頻率限制。
總結
本篇介紹了釘釘打卡小工具兩個方面的優化,還有配置部分的代碼我也做了精簡,可以更快的接入自己的釘釘應用。