头图

Hello everyone, my name is Yang Chenggong.

I wrote an article earlier, which introduced how to use Node.js + DingTalk API to implement a small tool for continuous reminder of attendance clock. Some students left a message saying why not directly call the DingTalk API to automatically punch in (I also thought about this). Unfortunately, I searched through DingTalk's documentation and couldn't find this API.

Besides, how could there be such an API? What do you think?

Some students pointed out the problem sternly: "I asked for leave and you keep reminding me? The token expires in a few hours!". In response to these two problems, we optimized the code based on the previous implementation and added two logics:

  1. When getting people who haven't punched in, filter the people who have asked for leave
  2. When the token expires, automatically refresh the token

If you haven't read the previous article, please read the first article of the punch card widget , the source code is on GitHub .

Next, we will implement the new requirements together and optimize the punch-in function.

Filter people who have taken leave

Use DingTalk API to get the punching status of some people. At present, our approach is to maintain the userid of the people who need to detect the punch-in status (our entire team) in a list, and then obtain the punch-in data of these people, so as to filter out the people who have not punched in.

The special case is that if a member of our group asks for leave today, he will be constantly reminded as a person who has not clocked in. In fact, we need to exclude the people who have asked for leave. The first step is to get the people who have asked for leave today.

The API to get the leave status is as follows:

API address: ${baseURL}/topapi/attendance/getleavestatus
Request method: POST
Document address: here

The request body of this API is an object with the following properties:

  • userid_list: query the userid list of leave status
  • start_time: query start time (working time of the day)
  • end_time: query end time (off-duty time of the day)
  • size: the number of returned items, the maximum is 20
  • offset: pagination, starting from 0

Write the get leave status as a separate method, the code is as follows:

 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);
  }
};

After executing the above method, you can get the users who have asked for leave on that day. Then, in the list of all users who need to detect the punch-in status, filter out the users who have asked for leave:

 // 需要检测打卡的 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 数组

This eliminates reminders for users who have taken leave.

DingTalk token automatically refreshes

When obtaining DingTalk API, you must first obtain the API call credentials (that is, access_token), which must be carried with each API call. However, this certificate has an expiration date, and the API will be prohibited from calling after the expiration date.

Therefore, a very important optimization point here is to automatically refresh the access_token.

How to do it? In fact, it is the same as the implementation in the front-end project. In the interceptor of axios , it is judged whether the access_token has expired. If it expires, it will be re-acquired, and then continue to execute the request.

First, write the get credentials as a separate method as follows:

 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);
  }
};

This method is mainly to call the API for obtaining credentials. After the call is successful, the access_token and the valid time will be returned. Here we need to set an expiration time, which is 当前时间+有效时间 , and generate a timestamp of the expiration time:

 Date.now() + result.data.expires_in * 1000,

There is also a DingToken class for obtaining and storing access_token, the code is as follows:

 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);
  }
}

The access_token and the expiration time are formed into a JSON string and stored in the file. Next, the JSON data can be obtained in the request interceptor of axios , and then judge whether the current time is greater than the expiration time.

If it is, then call the fetchToken() method again to generate a new token and continue to execute the request. The interceptor code is as follows:

 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;
});

With the logic written in the interceptor above, we don't need to care about access_token expiration. And we will only re-request after the token expires, so the call frequency limit will not be triggered.

Summarize

This article introduces the optimization of the DingTalk punch-in gadget in two aspects, and I have also simplified the code in the configuration part, so that you can access your own DingTalk application faster.

This code has been open sourced, and the address is on GitHub . You are welcome to try and star.


杨成功
3.9k 声望12k 粉丝

分享小厂可落地的前端工程与架构