大家好,我是程序员凌览。
每周都会为下一周的工作制定计划,也就是自我安排下周的任务。但由于缺乏提醒,我总是忘记执行,导致KPI受到影响。
为了解决这个提醒问题,我意识到肯定还有其他人和我一样面临这样的需求。因此,我计划开发一个网页应用,让用户能够通过简单的点击操作来设置钉钉机器人,实现周期性的提醒功能。
这个小工具的后端模块已经开发完成。然而,在与同事讨论这个工具时,我意外得知钉钉群的自定义机器人已经具备了类似的功能。
诺,就是这个:
个人产品很难与官方成熟的解决方案竞争。因此,这个产品的构想在刚刚萌芽时就夭折了。
接下来,我将分享这个小工具是如何一步步完成的。
钉钉自定义机器人加签处理
根据钉钉自定义机器人官方文档,创建自定义机器人后,我们需要在安全设置中选择“加签”选项。这意味着后端需要对发送给钉钉服务器的请求进行签名处理,以确保数据的安全性和完整性。
在Node.js中,我们可以使用内置的crypto
模块来实现这一功能,无需额外下载任何第三方库。以下是使用Node.js的crypto
模块进行签名处理的基本步骤:
const crypto = require('crypto');
const querystring = require('querystring');
const qs = require('qs');
const getUrl = ({ access_token, secret }) => {
//钉钉链接
const baseURL = 'https://oapi.dingtalk.com/robot/send';
const timestamp = Math.round(Date.now()).toString();
const stringToSign = `${timestamp}\n${secret}`;
const hmac = crypto.createHmac('sha256', secret);
hmac.update(stringToSign);
const hmacCode = hmac.digest();
// 将签名编码为 Base64 并进行 URL 编码
const sign = querystring.escape(Buffer.from(hmacCode).toString('base64'));
const query = qs.stringify({
access_token,
timestamp,
sign
})
return `${baseURL}?${query}`;
}
其中access_token
、secret
由钉钉机器人提供,复制出来即可:
自定义机器人消息数据处理
钉钉自定义机器人支持多种消息类型,用于发送不同格式的消息。为了简化操作,我们将自定义机器人需要发送的消息模板数据存储在一个JSON文件中,该文件位于:
database/db.json
{
"robot": [
{
"access_token": "xx",
"secret": "xx",
"msgtype": "markdown",
"cron": "0 0 16-21 * * 5-7",
"content": {
"markdown": {
"title": "排期提醒",
"text": "### 📆 周五排期提醒\n\n 您好!请查看本周的需求排期与任务管理情况,并确保所有任务按时完成。\n\n🔗 [需求排期与任务管理](https://alidocs.dingtalk.com/i/nodes/m9b)\n\n今日重点:\n- 检查本周任务完成情况\n- 准备下周工作计划\n\n注意事项:\n- 确保所有任务都有明确的截止日期和责任人\n- 及时更新任务进度,确保信息同步\n\n祝大家周末愉快,我们下周再见!\n\n @xx "
},
"at": {
"atMobiles": [
"xx"
]
}
}
}
]
}
在这个JSON文件中,cron
字段用于定义Cron表达式,这是一种用于指定定时任务执行时间的字符串表达式。Cron表达式由6个字段组成,分别对应秒、分钟、小时、天数、月份和星期几。每个字段都可以使用特定的符号来指定时间范围或间隔。如果您对Cron表达式不太熟悉,可以通过百度等搜索引擎进行了解。
为了操作这个JSON文件,使用了lowdb这个工具库。lowdb是一个轻量级的本地JSON数据库,它提供了简单的API来读写JSON文件。
const path = require('path');
const url = path.join(__dirname, './database/db.json')
module.exports = {
createLowDb: async () => {
const { JSONFilePreset } = await import('lowdb/node')
const defaultData = { robot: [] }
const db = await JSONFilePreset(url, defaultData)
return db
}
}
当时的想法是,用户通过我的页面填写机器人的配置信息后,这些数据将直接存储在JSON文件中。在项目初期,追求简单高效,没有必要引入复杂的数据库系统来处理这些数据。
注册周期性任务
为了实现周期性任务的注册,我们使用了node-schedule库。这个库允许我们在Node.js环境中轻松地设置和执行定时任务。通过Cron表达式,我们可以指定任务的执行时间,包括秒、分钟、小时、天、月和星期几。
首先,我们定义一个函数来注册周期性任务:
const schedule = require('node-schedule');
//省略
//注册一个周期性任务
const createJob = (dingRobot) => {
return schedule.scheduleJob(dingRobot.cron, async () => {
const url = getUrl({ access_token: dingRobot.access_token, secret: dingRobot.secret })
const params = {
msgtype: dingRobot.msgtype,
[dingRobot.msgtype]: dingRobot[dingRobot.msgtype],
at: dingRobot.at
}
const [err, result] = await to(axios.post(url, params))
if (err) {
console.err(err);
}
});
}
接下来,我们从JSON文件中读取所有周期性任务的配置,并循环注册这些任务:
const { createLowDb } = require('./db')
const schedule = require('node-schedule');
//注册多个周期性任务
const scheduleJob = async () => {
const db = await createLowDb()
const { robot = [] } = db.data
const scheduleJobs = robot.map(dingRobot => {
const job = createJob(dingRobot)
return {
job,
access_token: dingRobot.access_token,
secret: dingRobot.secret
}
})
return scheduleJobs
}
module.exports = {
createJob,
scheduleJob
}
最后,在程序启动时,执行初始化操作:
const express = require('express')
const app = express()
const port = 3000
let jobs = []
//省略
app.listen(port, async () => {
jobs = await scheduleJob()
console.log(`Example app listening on port ${port}`)
})
增删改周期性任务
通过创建一个简单的Web服务器,并添加相应的路由接口来实现增删改JSON文件内容的功能。以下是使用Node.js和Express框架来实现这个功能代码:
const { createLowDb } = require('./db')
//省略
const updateDb = async (type, data, index) => {
const db = await createLowDb()
if (eq(type, 'add')) {
await db.update(({ robot }) => robot.push(data))
return
}
if (eq(type, 'delete')) {
await db.update(({ robot }) => {
robot.splice(index, 1)
return robot
})
return
}
if (eq(type, 'set')) {
await db.update(({ robot }) => {
console.log(robot);
robot[index] = data
console.log(robot);
return robot
})
return
}
}
// 添加机器人
router.post('/robot', (req, res) => {
const { access_token = "", secret = "", msgtype = "text", cron = "", content = {} } = req.body
if (isEmpty(access_token) || isEmpty(secret) || isEmpty(msgtype) || isEmpty(cron) || isEmpty(content)) {
res.send({ code: 500, message: '参数错误' })
return
}
const findJob = jobs.find(job => {
return eq(job.access_token, access_token) && eq(job.secret, secret)
})
const params = {
access_token,
secret,
msgtype,
cron,
content
}
// 更改已存在的机器消息
if (findJob) {
findJob.job.cancel()
const index = jobs.indexOf(findJob)
jobs[index] = {
job: createJob(params),
access_token: params.access_token,
secret: params.secret
}
updateDb('set', params, index)
} else {
jobs.push(createJob(params))
updateDb('add', params)
}
res.send({ code: 200, message: 'success' })
})
这样后端部分功能完成。
前端页面和后端服务共用一个端口,并且前端页面放置在pages
目录下,这样,后端和前端就可以通过同一个端口对外提供服务,实现逻辑:
const express = require('express')
//省略
app.get("/", (req, res) => {
res.redirect('/index.html')
});
app.use(express.static(path.join(__dirname, './pages')))
部署应用
使用Docker部署一个Node应用,时区定为中国上海:
# 使用官方Node.js的Docker镜像作为基础镜像
FROM node:16-slim
# 设置工作目录为/app
WORKDIR /app
# 将package.json和package-lock.json(如果使用npm)复制到工作目录
COPY package*.json ./
# 安装项目依赖
RUN npm install
# 将项目中的所有文件复制到工作目录中
COPY . .
# 暴露容器的3000端口
EXPOSE 3000
# 定义环境变量
ENV NODE_ENV production
# 设置时区为上海
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
# 容器启动时执行的命令
CMD ["node", "index.js"]
在完成打包之后,把镜像推送到了阿里云容器镜像服务平台。接下来,在Linux服务器上执行拉取(pull)、运行(run)等操作,以部署应用。
完整代码链接:github.com/CatsAndMice…
最后
钉钉官方已经提供了解决方案来满足类似的需求,这是我之前没有预料到的,真是个疏忽。不过,偶尔编写一些工具类的代码,探索技术的可能性,确实是一件有趣的事情。