15
头图

Hello everyone, my name is Yang Chenggong.

Our company uses DingTalk to clock in attendance, and the personnel requirements are relatively strict. Two failures to clock in will be recorded as one day absence. But our group was so engrossed in work that we always forgot to punch in and out of get off work, and our monthly salary was deducted to the point of pain.

At the beginning, we all set up a clock-in alarm to remind us on time after get off work, but sometimes we work overtime and forget to clock in when we come home from overtime. Other times, Mizhi confidently thought that he had clocked in, and he looked at the attendance record the next day and found that he had not clocked in.

In order to completely solve this problem and keep our wallets, I developed a punch-in reminder tool, which allows the whole group to attend for three consecutive months!

The following describes how this gadget is implemented.

Small tool implementation ideas

First think about it: Why can't the alarm reminder be 100% useful?

  1. mechanical reminder

The alarm reminder is very mechanical. It is fixed at one point every day, and people will be immune after a long time. It's like the wake-up alarm has been used for a long time, and slowly the sound doesn't work for you, and you have to change the ringtone at this time.

  1. Reminders cannot be repeated

The alarm will only remind you once at a fixed time, there is no way to judge whether you have clocked in, and it will not intelligently find that you have not clocked in and remind you again.

Since the alarm can't do it, let's use the program to achieve it. According to the above two reasons, the reminder tool we want to implement must contain two functions:

  1. Detect whether the user has punched in, if not, it will be reminded, and if the user has punched in, no reminder.
  2. Loop detection for users who have not punched in, and repeat reminders until they punch in.

If these two functions can be realized, then the problem of forgetting to punch in is mostly solved.

The punch-in data needs to be obtained from DingTalk, and DingTalk has a push function. Therefore, our solution is to use Node.js + DingTalk API to realize check-in status detection and accurate reminder push.

Understanding DingTalk API

DingTalk is an enterprise version of instant messaging software. The biggest difference from WeChat is that it provides open capabilities, and APIs can be used to create groups, send messages and other functions, which means that using DingTalk can achieve highly customized communication capabilities.

The DingTalk APIs we use here mainly include the following:

  • get credentials
  • get user id
  • Check punch status
  • In-group message push
  • @ someone push

Before using the DingTalk API, you must first confirm that you have a company-level DingTalk account (if you have used the DingTalk check-in function, you usually have a company account), and the following steps are implemented under this account.

Apply for an open platform application

The first step in the development of DingTalk is to go to the DingTalk open platform to apply for an application and get the appKey and appSecret.

DingTalk open platform address: https://open.dingtalk.com/developer

After entering the platform, click "Developer Background", as shown below:

2022-08-10-06-22-22.png

The developer background is the place to manage the DingTalk application developed by yourself. After entering, select "Application Development -> Internal Development", as shown below:

2022-08-10-06-28-26.png

Entering this page may prompt that there is no permission for the time being. This is because the developer permission is required to develop enterprise DingTalk applications, and this permission needs to be added by the administrator in the background.

Administrator plus developer permissions:
Enter the OA management background , select the corresponding permissions under Settings - Permission Management - Management Group - Add Developer Permissions.

After entering, select [Create Application -> H5 Micro Application] and follow the prompts to create an application. After creation, you can see two key fields in [Application Information]:

  • AppKey
  • AppSecret

2022-08-14-10-40-02.png

These two fields are very important, and you need to pass them as parameters when obtaining the interface call credentials. AppKey is the unique identifier of the internal application of the enterprise, and AppSecret is the corresponding calling key.

Build server applications

DingTalk API needs to be called on the server side. That is to say, we need to build a server application to request the DingTalk API.

Remember not to call DingTalk API directly on the client side, because AppKey and AppSecret are both confidential and cannot be directly exposed on the client side.

We use the Express framework of Node.js to build a simple server-side application that interacts with the DingTalk API. The built Express directory structure is as follows:

 |-- app.js // 入口文件
|-- catch // 缓存目录
|-- router // 路由目录
|   |-- ding.js // 钉钉路由
|-- utils // 工具目录
|   |-- token.js // token相关

app.js is the entry file and the core logic of the application. The code is simply written as follows:

 const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cors = require('cors');

app.use(bodyParser.json());
app.use(cors());

// 路由配置
app.use('/ding', require('./router/ding'));

// 捕获404
app.use((req, res, next) => {
  res.status(404).send('Not Found');
});

// 捕获异常
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).send(err.inner || err.stack);
});

app.listen(8080, () => {
  console.log(`listen to http://localhost:8080`);
});

Another router/ding.js file is the Express standard routing file, where the relevant logic of the DingTalk API is written. The code infrastructure is as follows:

 // router/ding.js
var express = require('express');
var router = express.Router();

router.get('/', (req, res, next) => {
  res.send('钉钉API');
});

module.exports = router;

Now run the application:

 $ node app.js

Then visit http://localhost:8080/ding , the browser page displays the words "DingTalk API", indicating that the operation is successful.

Butt nail application

After a simple server application is built, it is ready to access the DingTalk API.

For the access steps, please refer to the development documentation. The documentation address is here .

1. Get API call credentials

DingTalk API needs to be authenticated before it can be called. The way to verify the permissions is to obtain an access_token according to the AppKey and AppSecret obtained in the previous step. This access_token is the calling credential of the DingTalk API.

When calling other APIs later, you only need to carry the access_token to verify the permissions.

DingTalk API is divided into two versions: new version and old version. For compatibility, we use the old version. The URL root path of the old API is https://oapi.dingtalk.com , and the variable baseURL is used below.

According to the documentation, the interface to get access_token is ${baseURL}/gettoken . Define a method to get token in utils/ding.js file, use GET request to get access_token, the code is as follows:

 const fetchToken = async () => {
  try {
    let params = {
      appkey: 'xxx',
      appsecret: 'xxx',
    };
    let url = `${baseURL}/gettoken`;
    let result = await axios.get(url, { params });
    if (result.data.errcode != 0) {
      throw result.data;
    } else {
      return result.data;
    }
  } catch (error) {
    console.log(error);
  }
};

After the above code is written, you can call the fetchToken function to obtain the access_token.

After the access_token is obtained, it needs to be persistently stored for subsequent use. On the browser side, we can save in localStorage, while on the Node.js side, the easiest way is to save directly in a file.

Write a class that saves the access_token as a file and can read it. The code is as follows:

 var fs = require('fs');
var path = require('path');

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

With that written, now we get the access_token and store:

 var res = await fetchToken();
if (res) {
  new DingToken().set(res.access_token);
}

When the following interface is called, the access_token can be obtained through new DingToken().get() .

2. Find the group member ID

With access_token, the first DingTalk API to call is to get the employee's userid. userid is the unique ID of an employee in DingTalk.

After we have the userid, we can get the punching status corresponding to the group members. The easiest way is to get the employee's userid through the mobile phone number, which can be found directly on DingTalk.

Query user documentation based on mobile phone number here .

The interface calling code is as follows:

 let access_token = new DingToken().get();
let params = {
  access_token,
};
axios
  .post(
    `${baseURL}/topapi/v2/user/getbymobile`,
    {
      mobile: 'xxx', // 用户手机号
    },
    { params },
  )
  .then((res) => {
    console.log(res);
  });

Through the above request method, get the userid of all group members one by one and save it, we will use it in the next step.

3. Get the punch status

After getting the userid list of the group members, we can get the punch-in status of all the group members.

To get the punch-in status of DingTalk, you need to apply for permission in the H5 app. Open the previously created application, click [Permission Management -> Attendance], and add all permissions in batches:

2022-08-14-10-35-18.png

Then enter [Development Management] and configure the server export IP. This IP refers to the IP address of the server we call DingTalk API. It can be filled in as 127.0.0.1 during development, and replaced with the real IP address after deployment.

After doing these preparations, we can get the punch card status. The API to get the punch status is as follows:

API address: ${baseURL}/attendance/list
Request method: POST

The request body of this API is an object, and the object must contain the following properties:

  • workDateFrom: The starting working day for querying attendance punch records.
  • workDateTo: Query the end working day of the attendance punch record.
  • userIdList: Query the user ID list of the user.
  • offset: data starting point, used for paging, just pass 0.
  • limit: Get the number of attendance records, the maximum is 50.

The fields here are explained. workDateFrom and workDateTo represent the time range for query attendance, because we only need to query the data of the day, so the event range is from 0:00 to 24:00 on the day.

userIdList is the userid list of all group members we got in the previous step.

Write the check-in status as a separate method, the code is as follows:

 const dayjs = require('dayjs');
const access_token = new DingToken().get();

// 获取打卡状态
const getAttendStatus = (userIdList) => {
  let params = {
    access_token,
  };
  let body = {
    workDateFrom: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
    workDateTo: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
    userIdList, // userid 列表
    offset: 0,
    limit: 40,
  };
  return axios.post(`${baseURL}/attendance/list`, body, { params });
};

The return result of querying the attendance status is a list, and the key fields of the list items are as follows:

  • userId: User ID of the puncher.
  • userCheckTime: The actual check-in time of the user.
  • timeResult: User punch-in result. Normal: Normal, NotSigned: Not clocked.
  • checkType: Attendance type. OnDuty: Go to work, OffDuty: Go off work.

For the meaning of other fields, please refer to the documentation

The above four fields can easily determine who should punch in and whether the punch is normal, so that we can filter out users who have not punched in, and accurately remind these users who have not punched in.

There are two types of check-in status:

  • punch card
  • punch card after get off work

Punching to and from get off work needs to filter different returned data. Assuming that the acquired punch-in data is stored in the variable attendList, the acquisition method is as follows:

 // 获取上班打卡记录
const getOnUids = () =>
  attendList
    .filter((row) => row.checkType == 'OnDuty')
    .map((row) => row.userId);

// 获取下班打卡记录
const getOffUids = () =>
  attendList
    .filter((row) => row.checkType == 'OffDut')
    .map((row) => row.userId);

Get the users who have punched in, and then find the users who have not punched in, you can send a notification reminder.

4. Send reminder notifications

The most commonly used message push method in DingTalk is to add a bot to the group chat and send a message to the bot's webhook address to implement custom push.

Or enter the H5 application created earlier, find [Application Function -> Message Push -> Robot] in the menu, and configure the robot according to the prompts.

2022-08-17-23-38-21.png

After the robot is created, open the DingTalk group where the group members belong (either an existing group or a new group can be created). Click [Group Settings -> Smart Group Assistant -> Add Robot], select the robot you just created, and you can bind the robot to the group.

2022-08-18-07-50-46.png

After binding the robot, click the robot settings, you will see a webhook address, you can send a message to the group chat by requesting this address. The corresponding API is as follows:

API address: ${baseURL}/robot/send?access_token=xxx
Request method: POST

Now send a "I am a punching robot", the implementation code is as follows:

 const sendNotify = (msg, atuids = []) => {
  let access_token = 'xxx'; // Webhook 地址上的 access_token
  // 消息模版配置
  let infos = {
    msgtype: 'text',
    text: {
      content: msg,
    },
    at: {
      atUserIds: atuids,
    },
  };
  // API 发送消息
  axios.post(`${baseURL}/robot/send`, infos, {
    params: { access_token },
  });
};
sendNotify('我是打卡机器人');

Explain: The atUserIds attribute in the code represents the user who wants to @, and its value is an array of userid, which can @ certain members of the group, so that the message push will be more accurate.

After sending, you will receive a message in the DingTalk group, the effect is as follows:

2022-08-18-14-44-54.png

Comprehensive code implementation

In the previous steps, the DingTalk application was created, the check-in status was obtained, and the group notification was sent with the robot. Now combine these functions and write an interface that checks the attendance status and sends reminders to users who have not clocked in.

Create a routing method in the routing file router/ding.js to implement this function:

 var dayjs = require('dayjs');

router.post('/attend-send', async (req, res, next) => {
  try {
    // 需要检测打卡的 userid 数组
    let alluids = ["xxx", "xxxx"];
    // 获取打卡状态
    let attendList = await getAttendStatus(alluids);
    // 是否9点前(上班时间)
    let isOnDuty = dayjs().isBefore(dayjs().hour(9).minute(0));
    // 是否18点后(下班时间)
    let isOffDuty = dayjs().isAfter(dayjs().hour(18).minute(0));
    if (isOnDuty) {
      // 已打卡用户
      let uids = getOnUids(attendList);
      if (alluids.length > uids.length) {
        // 未打卡用户
        let txuids = alluids.filter((r) => !uids.includes(r));
        sendNotify("上班没打卡,小心扣钱!", txuids);
      }
    } else if (isOffDuty) {
      // 已打卡用户
      let uids = getOffUids(attendList);
      if (alluids.length > uids.length) {
        // 未打卡用户
        let txuids = alluids.filter((r) => !uids.includes(r));
        sendNotify("下班没打卡,小心扣钱!", txuids);
      }
    } else {
      return res.send("不在打卡时间");
    }
    res.send("没有未打卡的同学");
  } catch (error) {
    res.status(error.status || 500).send(error);
  }
});

After the above interface is written, we only need to call this interface to realize the automatic detection of punching in or out of get off work. If there are unchecked team members, the robot will send a notification in the group to remind and @ unchecked team members.

 # 调用接口
$ curl -X POST http://localhost:8080/ding/attend-send

The function of checking the punching status and reminding has been realized, and now there is still a "cycle reminder" function.

The idea of the circular reminder is to call the interface every few minutes within a certain period of time. If an unchecked state is detected, it will be reminded cyclically.

Assuming that the commute time is 9:00 am and 18:00 pm, the detection time period can be divided into:

  • Go to work: between 8:30-9:00, check every 5 minutes;
  • After get off work: between 18:00-19:00, the test is every 10 minutes;

It is relatively urgent to check in at work, so the time detection is short and the frequency is high. The clock-in after get off work is relatively loose, and the get off work-off time is not fixed, so the detection time is longer and the frequency is lower.

After determining the detection rules, we use the Linux timing task crontab to achieve the above functions.

First, deploy the Node.js code written above to the Linux server. After deployment, you can call the interface inside Linux.

crontab configuration parsing

Briefly talk about how to configure crontab scheduled tasks. Its configuration method is one task per line, and the configuration fields of each line are as follows:

 // 分别表示:分钟、小时、天、月、周、要执行的命令
minute hour day month weekday cmd

Each field is represented by a specific number, or * if you want to match all. The configuration of work punch detection is as follows:

 29-59/5 8 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

The above 29-59/5 8 means that it is executed every 5 minutes between 8:29 and 8:59; 1-5 means Monday to Friday, so it is configured.

For the same reason, the configuration of check-in detection after get off work is as follows:

 */10 18-19 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

Execute in Linux crontab -e open the editing page, write the above two configurations and save, and then check whether it takes effect:

 $ crontab -l
29-59/5 8 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send
*/10 18-19 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

If you see the above output, it means that the scheduled task was created successfully.

Now every day before and after get off work, the gadget will automatically detect the punch-in status of team members and remind them cyclically. The final effect is as follows:

image.png

Summarize

This gadget is based on DingTalk API + Node.js implementation. The idea is interesting and solves practical problems. And this small project is very suitable for learning Node.js, the code is concise and clean, easy to understand and read.

The small project has been open sourced, and the open source address is:

https://github.com/ruidoc/attend-robot

Welcome everyone to start, thank you.


杨成功
3.9k 声望12k 粉丝

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