feat: 完成定时卡片发送逻辑

This commit is contained in:
zhaoyingbo 2023-08-26 14:43:23 +08:00
parent 9bf37ce9d3
commit dddb66cc64
8 changed files with 156 additions and 163 deletions

View File

@ -19,7 +19,9 @@
[ ] 用药提醒的卡片模板,确认、取消、延迟
[ ] 看看提醒统计的卡片需要什么数据应该是需要单建统计数据表看看能不能融合进Remind表加个needRepeat字段
[ ] 看看提醒统计的卡片需要什么数据应该是需要单建统计数据表看看能不能融合进Remind表加个needReply字段
[ ] 通过getInfo指令在对话里返回请求的数据
## 待定TODO
@ -48,7 +50,7 @@ interface Remind {
*/
id: string;
/**
* 所有者信息,绑定用户表的open_id
* 所有者信息绑定用户表的id
*/
owner: string;
/**
@ -68,6 +70,10 @@ interface Remind {
* 是否需要回复,不需要回复的也不会重复提醒
*/
needReply: boolean;
/**
* 延迟时间
*/
delayTime: number;
/**
* 卡片信息,用于绘制初始卡片、确认卡片、取消卡片、延迟卡片
*/

View File

@ -1,9 +1,14 @@
const schedule = require('node-schedule');
const { resetAccessToken } = require('./accessToken');
const { sendCurrTimeReminds } = require('../schedule/remind');
exports.initSchedule = async () => {
// 定时任务每15分钟刷新一次token
schedule.scheduleJob('*/15 * * * *', resetAccessToken);
// 定时任务,每分钟检查一次是否有需要提醒的卡片
schedule.scheduleJob('* * * * *', sendCurrTimeReminds);
// 立即执行一次
resetAccessToken()
sendCurrTimeReminds()
}

View File

@ -1,30 +1,59 @@
const { genCard } = require("../utils/genCard")
const { getCurrRemind } = require("../utils/pb")
const { sendCard } = require("../utils/sendCard")
const { getDelayedRemindTime, getNextRemindTime } = require("../utils/nextRemindTime")
const { getCurrRemind, updateNextRemindTime, getPendingRemindRecord, updateRemindRecord, createRemindRecord } = require("../utils/pb")
const { sendCard, updateCard } = require("../utils/sendCard")
const { getNowStr } = require("../utils/time")
/**
* 更新上一个pending状态的卡片至delayed
*/
const updateLastPendingCardToDelayed = async (remind) => {
const record = await getPendingRemindRecord(remind.id)
if (!record) return
// 当前时间即为回复时间
const interactTime = getNowStr()
// 创建delayed状态的卡片
const card = genCard(remind, 'delayed', null, interactTime)
// 更新飞书的卡片
await updateCard(record.messageId, card)
// 更新remindRecord
const newRecord = {
...record,
status: 'delayed',
interactTime,
}
await updateRemindRecord(record.id, newRecord)
}
/**
* 处理提醒包括创建卡片更新下一次提醒时间更新上一个padding状态的卡片至delayed
* 处理提醒包括创建卡片更新下一次提醒时间更新上一个pending状态的卡片至delayed
* @param {Remind} remind 提醒信息
*/
const manageRemind = async (remind) =>{
const manageRemind = async (remind) => {
// 更新上一个pending状态的卡片至delayed
await updateLastPendingCardToDelayed(remind)
// 生成卡片
const card = genCard(remind)
const card = genCard(remind, 'pending', null, null)
// 发送卡片
await sendCard(card)
const messageId = await sendCard(remind.subscriber.type, remind.subscriber.id, 'interactive', card)
// 创建remindRecord
await createRemindRecord(remind.id, messageId)
// 获取下一次提醒时间不需要回复的直接时下一次提醒时间需要回复的则是当前时间延后10min
const nextRemindTime = remind.needReply ? getDelayedRemindTime(remind.delayTime) : getNextRemindTime(remind)
// 更新下一次提醒时间
await updateNextRemindTime(remind)
// 更新上一个padding状态的卡片至delayed
await updatePrevPaddingCard(remind)
await updateNextRemindTime(remind.id, nextRemindTime)
}
/**
* 处理当前分钟需要处理的卡片
* 发送提醒更新上一个padding状态的卡片至delayed
* 发送提醒更新上一个pending状态的卡片至delayed
*/
const sendCurrTimeReminds = async () => {
module.exports.sendCurrTimeReminds = async () => {
const remindList = await getCurrRemind()
// 没有需要提醒的卡片
if (!remindList.length) return
// 处理提醒
for (const remind of remindList) {
await manageRemind(remind)
}
}

8
typings.d.ts vendored
View File

@ -1,3 +1,5 @@
/**
*
*/
@ -7,7 +9,7 @@ interface Remind {
*/
id: string;
/**
* open_id
* id
*/
owner: string;
/**
@ -27,6 +29,10 @@ interface Remind {
*
*/
needReply: boolean;
/**
*
*/
delayTime: number;
/**
*
*/

View File

@ -160,7 +160,7 @@ const genRemindInfo = (
* @param {*} interactInfo 交互信息
* @param {*} interactTime 交互时间在上层已经转成 yyyy-MM-dd HH:mm 格式了
*/
const genCard = (
module.exports.genCard = (
remindInfo,
cardType,
interactInfo,
@ -277,151 +277,8 @@ const genCard = (
if (remindInfoDom) {
elements.push(remindInfoDom);
}
return {
return JSON.stringify({
elements,
header,
};
};
const testGenRemindTimeText = () => {
const testCases = [
{
frequency: "single",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "05-01",
expect: "05-01 10:00",
},
{
frequency: "daily",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "",
expect: "每天 10:00",
},
{
frequency: "weekly",
time: "10:00",
dayOfWeek: 1,
dayOfMonth: 0,
dayOfYear: "",
expect: "周一 10:00",
},
{
frequency: "monthly",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 1,
dayOfYear: "",
expect: "每月1日 10:00",
},
{
frequency: "yearly",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "05-03",
expect: "每年05-03 10:00",
},
{
frequency: "workday",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "",
expect: "工作日 10:00",
},
{
frequency: "holiday",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "",
expect: "节假日 10:00",
},
];
testCases.forEach((testCase) => {
const { frequency, time, dayOfWeek, dayOfMonth, dayOfYear, expect } =
testCase;
const result = genRemindTimeText(
frequency,
time,
dayOfWeek,
dayOfMonth,
dayOfYear
);
if (result !== expect) {
console.log(
`genRemindTimeText failed, expect: ${expect}, result: ${result}`
);
} else {
console.log("genRemindTimeText success");
}
});
};
const testGenCard = () => {
const remindInfo = {
cardInfo: {
title: "🎉 帝君喊你吃药!",
imageKey: "img_v2_a74b2092-9f61-44d2-ab4c-45f9c4e083el",
content: "**本次包含药品:**\n美沙拉嗪 \\* 1\n复方谷氨酰胺 \\* 4",
confirmText: "我吃完了",
delayText: "稍后提醒",
cancelText: "淦,忘吃了",
},
templateInfo: null,
needReply: true,
frequency: "single",
time: "10:00",
dayOfWeek: 0,
dayOfMonth: 0,
dayOfYear: "05-01",
}
const cardType = "pending";
const interactInfo = null;
const interactTime = null;
const expect = {"elements":[{"alt":{"content":"","tag":"plain_text"},"img_key":"img_v2_a74b2092-9f61-44d2-ab4c-45f9c4e083el","tag":"img"},{"tag":"div","text":{"content":"**本次包含药品:**\n美沙拉嗪 \\* 1\n复方谷氨酰胺 \\* 4","tag":"lark_md"}},{"tag":"action","actions":[{"tag":"button","text":{"tag":"plain_text","content":"我吃完了"},"type":"primary","value":{"result":"confirmed","text":"我吃完了"}},{"tag":"button","text":{"tag":"plain_text","content":"稍后提醒"},"type":"default","value":{"result":"delayed","text":"稍后提醒"}},{"tag":"button","text":{"tag":"plain_text","content":"淦,忘吃了"},"type":"danger","value":{"result":"canceled","text":"淦,忘吃了"}}]}],"header":{"title":{"content":"🎉 帝君喊你吃药!","tag":"plain_text"},"template":"blue"}}
const result = genCard(remindInfo, cardType, interactInfo, interactTime);
if (JSON.stringify(result) !== JSON.stringify(expect)) {
console.log(`genCard failed, expect: ${JSON.stringify(expect)}, result: ${JSON.stringify(result)}`);
} else {
console.log("genCard success");
}
const remindInfo2 = {
cardInfo: {
title: "🎉 帝君喊你吃药!",
imageKey: "img_v2_a74b2092-9f61-44d2-ab4c-45f9c4e083el",
content: "**本次包含药品:**\n美沙拉嗪 \\* 1\n复方谷氨酰胺 \\* 4",
confirmText: "我吃完了",
delayText: "稍后提醒",
cancelText: "淦,忘吃了",
},
templateInfo: null,
needReply: true,
frequency: "weekly",
time: "10:00",
dayOfWeek: 1,
dayOfMonth: 0,
dayOfYear: "",
}
const cardType2 = "confirmed";
const interactInfo2 = {
result: "confirmed",
text: "我吃完了",
}
const interactTime2 = "05-01 10:00";
console.log(JSON.stringify(genCard(remindInfo2, cardType2, interactInfo2, interactTime2)));
};
module.exports = {
genCard,
}
if (require.main === module) {
testGenRemindTimeText();
testGenCard();
}

View File

@ -57,7 +57,7 @@ const getNextday = (isWorkday = true) => {
* @param {Remind} remind 提醒信息
* @returns {string} 下次提醒时间, 格式为yyyy-MM-dd HH:mm
*/
const getNextRemindTime = (remind) => {
module.exports.getNextRemindTime = (remind) => {
const { frequency, time, dayOfWeek, dayOfMonth, dayOfYear } = remind;
const now = new Date();
const nowDay = now.getDay();
@ -66,7 +66,6 @@ const getNextRemindTime = (remind) => {
const nowYear = now.getFullYear();
const nowHour = now.getHours();
const nowMinute = now.getMinutes();
console.log(nowDay, nowDate, nowMonth, nowYear, nowHour, nowMinute)
if (frequency === 'daily') {
// 判断当前时间是否已经过了今天的提醒时间
@ -134,3 +133,18 @@ const getNextRemindTime = (remind) => {
return `${getNextday(false)} ${time}`
}
}
/**
* 获取delayTime分钟delay之后的提醒时间
* @param {number} delayTime 延迟时间, 单位为分钟, 1 <= delayTime
* @returns {string} delay的提醒时间, 格式为yyyy-MM-dd HH:mm
*/
module.exports.getDelayedRemindTime = (delayTime) => {
const nextRemindTime = new Date(new Date().getTime() + delayTime * 60 * 1000)
const nextRemindYear = nextRemindTime.getFullYear();
const nextRemindMonth = nextRemindTime.getMonth() + 1;
const nextRemindDate = nextRemindTime.getDate();
const nextRemindHour = nextRemindTime.getHours();
const nextRemindMinute = nextRemindTime.getMinutes();
return `${nextRemindYear}-${nextRemindMonth}-${nextRemindDate} ${nextRemindHour}:${nextRemindMinute}`
}

View File

@ -32,4 +32,49 @@ module.exports.getCurrRemind = async () => {
filter: `nextRemindTime = "${nowStr}" && enabled = true`,
})
return items
}
}
/**
* 更新指定Remind的下次提醒时间
* @param {Remind} remindId 提醒信息Id
* @param {string} nextRemindTime 下次提醒时间
*/
module.exports.updateNextRemindTime = async (remindId, nextRemindTime) => {
await pb.collection('remind').update(remindId, { nextRemindTime })
}
/**
* 获取指定remind的pending状态的remindRecord
* @param {string} remindId remind的id
* @returns {RemindRecord | null} remindRecord
*/
module.exports.getPendingRemindRecord = async (remindId) => {
const record = await pb.collection('remindRecord').getFirstListItem(
`remindId = "${remindId}" && status = "pending"`
)
return record
}
/**
* 创建remindRecord
*/
module.exports.createRemindRecord = async (remindId, messageId) => {
const remindRecord = {
remindId,
messageId,
status: 'pending',
remindTime: getNowStr(),
interactTime: '',
result: '',
}
await pb.collection('remindRecord').create(remindRecord)
}
/**
* 修改指定remindRecord
* @param {string} id remindRecord的id
* @param {RemindRecord} record remindRecord的信息
*/
module.exports.updateRemindRecord = async (id, record) => {
await pb.collection('remindRecord').update(id, record)
}

View File

@ -3,6 +3,14 @@ const {
getTenantAccessToken
} = require('./pb');
/**
* 发送卡片
* @param {string} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id
* @param {string} receive_id 消息接收者的IDID类型应与查询参数receive_id_type 对应
* @param {string} msg_type 消息类型 包括textpostimagefileaudiomediastickerinteractiveshare_chatshare_user
* @param {string} content 消息内容JSON结构序列化后的字符串不同msg_type对应不同内容
* @returns {string} 消息id
*/
module.exports.sendCard = async (receive_id_type, receive_id, msg_type, content) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`
const tenant_access_token = await getTenantAccessToken();
@ -18,4 +26,27 @@ module.exports.sendCard = async (receive_id_type, receive_id, msg_type, content)
})
const data = await res.json();
console.log('sendCard success', data);
return data.data.message_id;
}
/**
* 更新卡片
* @param {string} message_id 消息id
* @param {*} content 消息内容JSON结构序列化后的字符串不同msg_type对应不同内容
*/
module.exports.updateCard = async (message_id, content) => {
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}`
const tenant_access_token = await getTenantAccessToken();
const header = {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${tenant_access_token}`
}
const body = { content }
const res = await fetch(URL, {
method: 'PATCH',
headers: header,
body: JSON.stringify(body)
})
const data = await res.json();
console.log('updateCard success', data);
}