小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习egg,里面有用到定时任务,于是决定尝试一把 egg 实现
环境准备
操作系统:支持 macOS,Linux,Windows
运行环境:建议选择 node LTS 版本,最低要求 8.x。
创建egg项目和目录结构介绍
运行
本地开发
$ npm i
$ npm run dev
$ open http://localhost:7001/
部署生产
$ npm start
$ npm stop
控制器
class HomeController extends Controller {
async send() {
const { ctx, app } = this;
ctx.body = app.config;
const result = await ctx.service.sendmsg.sendOut();
ctx.logger.info('主动触发,发送模板消息 结果: %j', result);
ctx.body = result;
ctx.set('Content-Type', 'application/json');
}
}
service服务层
// 时间处理
const moment = require('moment');
class sendmsg extends Service {
// 发送模板消息给媳妇儿
async sendOut() {
const { ctx, app } = this;
const token = await this.getToken();
const data = await this.getTemplateData();
ctx.logger.info('获取token 结果: %j', token);
// 模板消息接口文档
const users = app.config.weChat.users;
const promise = users.map(id => {
ctx.logger.info('--------------开始发送每日提醒-----------------------------------------------: %j', id);
data.touser = id;
return this.toWechart(token, data);
});
const results = await Promise.all(promise);
ctx.logger.info('--------------结束发送每日提醒->结果-----------------------------------------------: %j', results);
return results;
}
// 通知微信接口
async toWechart(token, data) {
...
}
// 获取token
async getToken() {
...
}
// 组装模板消息数据
async getTemplateData() {
...
}
// 获取天气
async getWeather(city = '深泽') {
...
}
// 获取 下次发工资 还有多少天
getWageDay() {
...
}
// 获取距离 下次结婚纪念日还有多少天
getMarryDay() {
...
}
// 获取 距离 下次生日还有多少天
getbirthday() {
...
}
// 获取 相恋天数
getLoveDay() {
...
}
// 获取 相恋几年了
getLoveYear() {
...
}
// 获取是第几个生日
getBirthYear() {
...
}
// 获取是第几个结婚纪念日
getMarryYear() {
...
}
// 获取 每日一句
async getOneSentence() {
...
}
// 获取时间日期
getDatetime() {
...
}
}
发送模板消息
async toWechart(token, data) {
// 模板消息接口文档
const url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' + token;
const result = await this.ctx.curl(url, {
method: 'POST',
data,
dataType: 'json',
headers: {
'Content-Type': 'application/json',
},
});
return result;
}
获取Access token
async getToken() {
const { app } = this;
const url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' + app.config.weChat.appld + '&secret=' + app.config.weChat.secret;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result.status === 200) {
return result.data.access_token;
}
}
组装模板消息数据
async getTemplateData() {
const { app } = this;
// 判断所需 模板
// 发工资模板 getWageDay == 0 wageDay
// 结婚纪念日模板 getMarryDay == 0 marry
// 生日 模板 getbirthday == 0 birthday
// 正常模板 daily
const wageDay = this.getWageDay();
const marry = this.getMarryDay();
const birthday = this.getbirthday();
const data = {
topcolor: '#FF0000',
data: {},
};
// 发工资模板
if (!wageDay) {
data.template_id = app.config.weChat.wageDay;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
};
} else if (!marry) {
// 结婚纪念日模板
data.template_id = app.config.weChat.marry;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
anniversary: {
value: this.getMarryYear(),
color: '#ff3399',
},
year: {
value: this.getLoveYear(),
color: '#ff3399',
},
};
} else if (!birthday) {
// 生日模板
data.template_id = app.config.weChat.birthday;
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
individual: {
value: this.getBirthYear(),
color: '#ff3399',
},
};
} else {
// 正常模板
data.template_id = app.config.weChat.daily;
// 获取天气
const getWeather = await this.getWeather();
// 获取每日一句
const message = await this.getOneSentence();
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
love: {
value: this.getLoveDay(),
color: '#ff3399',
},
wage: {
value: wageDay,
color: '#66ff00',
},
birthday: {
value: birthday,
color: '#ff0033',
},
marry: {
value: marry,
color: '#ff0033',
},
wea: {
value: getWeather.wea,
color: '#33ff33',
},
tem: {
value: getWeather.tem,
color: '#0066ff',
},
airLevel: {
value: getWeather.air_level,
color: '#ff0033',
},
tem1: {
value: getWeather.tem1,
color: '#ff0000',
},
tem2: {
value: getWeather.tem2,
color: '#33ff33',
},
win: {
value: getWeather.win,
color: '#3399ff',
},
message: {
value: message,
color: '#8C8C8C',
},
};
}
return data;
}
获取天气
async getWeather(city = '石家庄') {
const { app } = this;
const url = 'https://www.tianqiapi.com/api?unescape=1&version=v6&appid=' + app.config.weather.appid + '&appsecret=' + app.config.weather.appsecret + '&city=' + city;
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
console.log(result.status);
// "wea": "多云",
// "tem": "27", 实时温度
// "tem1": "27", 高温
// "tem2": "17", 低温
// "win": "西风",
// "air_level": "优",
if (result && result.status === 200) {
return result.data;
}
return {
city,
wea: '未知',
tem: '未知',
tem1: '未知',
tem2: '未知',
win: '未知',
win_speed: '未知',
air_level: '未知',
};
}
获取 下次发工资 还有多少天
getWageDay() {
const { app } = this;
const wage = app.config.time.wageDay;
// 获取日期 day
// 如果在 wage号之前或等于wage时 那么就用 wage-day
// 如果在 wage号之后 那么就用 wage +(当前月总天数-day)
// 当日 日期day
const day = moment().date();
// 当月总天数
const nowDayTotal = moment().daysInMonth();
// // 下个月总天数
// const nextDayTotal = moment().month(moment().month() + 1).daysInMonth();
let resultDay = 0;
if (day <= wage) {
resultDay = wage - day;
} else {
resultDay = wage + (nowDayTotal - day);
}
return resultDay;
}
获取距离 下次结婚纪念日还有多少天
getMarryDay() {
const { app } = this;
const marry = app.config.time.marry;
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(marry).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年结婚纪念日时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 今天的结婚纪念日 有没有过,如果已经过去(now>nowTimeNumber),resultMarry日期为明年的结婚纪念日
// 如果还没到,则 结束日期为今年的结婚纪念日
let resultMarry = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年纪念日
resultMarry = moment((y + 1) + mmdd).valueOf();
}
return moment(moment(resultMarry).format()).diff(moment(now).format(), 'day');
}
获取 距离 下次生日还有多少天
getbirthday() {
const { app } = this;
const birthday = app.config.time.birthday[moment().year()];
// 获取当前时间戳
const now = moment(moment().format('YYYY-MM-DD')).valueOf();
// 获取纪念日 月-日
const mmdd = moment(birthday).format('-MM-DD');
// 获取当年
const y = moment().year();
// 获取今年生日 时间戳
const nowTimeNumber = moment(y + mmdd).valueOf();
// 判断 生日 有没有过,如果已经过去(now>nowTimeNumber),resultBirthday日期为明年的生日 日期
// 如果还没到,则 结束日期为今年的目标日期
let resultBirthday = nowTimeNumber;
if (now > nowTimeNumber) {
// 获取明年目标日期
resultBirthday = moment(app.config.time.birthday[y + 1]).valueOf();
}
return moment(moment(resultBirthday).format()).diff(moment(now).format(), 'day');
}
获取 相恋天数
getLoveDay() {
const { app } = this;
const loveDay = app.config.time.love;
return moment(moment().format('YYYY-MM-DD')).diff(loveDay, 'day');
}
获取 相恋几年了
getLoveYear() {
const { app } = this;
const loveDay = app.config.time.love;
return moment().year() - moment(loveDay).year();
}
获取是第几个生日
getBirthYear() {
const { app } = this;
const birthYear = app.config.time.birthYear;
return moment().year() - birthYear;
}
获取是第几个结婚纪念日
getMarryYear() {
const { app } = this;
const marry = app.config.time.marry;
return moment().year() - moment(marry).year();
}
获取 每日一句
async getOneSentence() {
const url = 'https://v1.hitokoto.cn/';
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
if (result && result.status === 200) {
return result.data.hitokoto;
}
return '今日只有我爱你!';
}
获取时间日期
getDatetime() {
console.log('moment().weekday()', moment().weekday());
const week = {
1: '星期一',
2: '星期二',
3: '星期三',
4: '星期四',
5: '星期五',
6: '星期六',
0: '星期日',
};
return moment().format('YYYY年MM月DD日 ') + week[moment().weekday()];
}
定时任务和主动触发
定时任务
└── app
└── schedule
└── update_cache.js
class UpdateCache extends Subscription {
// 通过 schedule 属性来设置定时任务的执行间隔等配置
static get schedule() {
return {
cron: '0 30 7 * * *', // 每天的7点30分0秒执行
// interval: '1m', // 1 分钟间隔
type: 'all', // 指定所有的 worker 都需要执行
};
}
// subscribe 是真正定时任务执行时被运行的函数
async subscribe() {
const { ctx } = this;
const result = await ctx.service.sendmsg.send();
ctx.logger.info('定时任务执行消息提醒 结果: %j', result);
}
}
日志中 可以查看 定时任务的 执行记录
└── logs
└── serves
└── serves-web
主动发送
请求或浏览器访问 http://localhost:7001/send
配置文件说明
└── logs
└── config.default.js
天气秘钥
// 天气接口配置
config.weather = {
appid: '*******',
appsecret: '*******',
};
特殊 时间点设置
下方是 birthday生日,因老家都是过阴历生日,不好处理,暂时写死的
// 时间
config.time = {
wageDay: 15, // 工资日
love: '2017-06-09', // 相爱日期
marry: '2021-11-27', // 结婚纪念日
birthday: {
2021: '2021-04-17',
2022: '2022-04-06',
2023: '2023-04-25',
2024: '2024-04-14',
2025: '2025-04-03',
2026: '2026-04-22',
}, // 每年生日 阳历
birthYear: '1995-03-06',
};
微信公众号 配置
因个人只能申请订阅号,而订阅号不支持发送模板消息,所以在此使用的测试的微信公众号,有微信号都可以申请,免注册,扫码登录
无需公众帐号、快速申请接口测试号
直接体验和测试公众平台所有高级接口
// 测试 微信公众号
config.weChat = {
appld: '**********',
secret: '**********',
// 用户的openid
users: [
'**********************',
'**********************',
'**********************',
'**********************'
],
daily: '************', // 普通模板
marry: ''************',', // 结婚纪念日模板
wageDay: ''************',', // 工资日模板
birthday: ''************',', // 生日模板
};
微信消息模板
这个需要在 上文提到的 微信公众平台测试账号 单独设置
以下是 我用的模板
正常模板
{{dateTime.DATA}}
今天是 我们相恋的第{{love.DATA}}天
距离上交工资还有{{wage.DATA}}天
距离你的生日还有{{birthday.DATA}}天
距离我们结婚纪念日还有{{marry.DATA}}天
今日天气 {{wea.DATA}}
当前温度 {{tem.DATA}}度
最高温度 {{tem1.DATA}}度
最低温度 {{tem2.DATA}}度
空气质量 {{airLevel.DATA}}
风向 {{win.DATA}}
每日一句
{{message.DATA}}
发工资模板
{{dateTime.DATA}}
老婆大人,今天要发工资了,预计晚九点前会准时上交,记得查收!
生日 模板
{{dateTime.DATA}}
听说今天是你人生当中第 {{individual.DATA}} 个生日?天呐,
我差点忘记!因为岁月没有在你脸上留下任何痕迹。
尽管,日历告诉我:你又涨了一岁,但你还是那个天真可爱的小妖女,生日快乐!
结婚纪念日
{{dateTime.DATA}}
今天是结婚{{anniversary.DATA}}周年纪念日,在一起{{year.DATA}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。
展示效果
天气接口 修改
上面的提到的接口已经不能用(小公司就是不靠谱 T_T),,目前改成了百度地图开放平台的 的天气服务api
需要的改动是
config.default.js
百度免费接口 没有空气质量信息,我改了模板,原来的空气质量air_level字段 改为了 wind_class
// 天气接口配置高德api
config.weather = {
ak: '******',
code: {
深泽: 130128,
石家庄: 130100,
北京: 110100,
},
};
模板修改
{{dateTime.DATA}}
今天是 我们相恋的第{{love.DATA}}天
距离上交工资还有{{wage.DATA}}天
距离你的生日还有{{birthday.DATA}}天
距离我们结婚纪念日还有{{marry.DATA}}天
今日天气 {{wea.DATA}}
当前温度 {{tem.DATA}}度
最高温度 {{tem1.DATA}}度
最低温度 {{tem2.DATA}}度
风向 {{win.DATA}}
风力等级 {{wind_class.DATA}}
每日一句
{{message.DATA}}
offiaccount.js
// 获取天气
async getWeather(city = '深泽') {
try {
const { app } = this;
// https://api.map.baidu.com/weather/v1/?district_id=130128&data_type=all&ak=bGjmaBLLzlBZXTiAkOwSqiVjftZlg17O
const url = 'https://api.map.baidu.com/weather/v1/?data_type=all&ak=' + app.config.weather.ak + '&district_id=' + app.config.weather.code[city];
const result = await this.ctx.curl(url, {
method: 'get',
dataType: 'json',
});
// "wea": "多云",
// "tem": "27", 实时温度
// "tem1": "27", 高温
// "tem2": "17", 低温
// "win": "西风",
// "air_level": "优",
if (result && result.data && result.data.status === 0) {
const now = result.data.result.now;
const forecasts = result.data.result.forecasts[0];
return {
wea: now.text,
tem: now.temp,
tem1: forecasts.high,
tem2: forecasts.low,
win: now.wind_dir,
wind_class: now.wind_class,
};
}
} catch (error) {
return {
wea: '未知',
tem: '未知',
tem1: '未知',
tem2: '未知',
win: '未知',
wind_class: '未知',
};
}
}
// 拼凑模板消息内容
async getTemplateData() {
...
// 获取天气
const getWeather = await this.getWeather();
// 获取每日一句
const message = await this.getOneSentence();
data.data = {
dateTime: {
value: this.getDatetime(),
color: '#cc33cc',
},
love: {
value: this.getLoveDay(),
color: '#ff3399',
},
wage: {
value: wageDay,
color: '#66ff00',
},
birthday: {
value: birthday,
color: '#ff0033',
},
marry: {
value: marry,
color: '#ff0033',
},
wea: {
value: getWeather.wea,
color: '#33ff33',
},
tem: {
value: getWeather.tem,
color: '#0066ff',
},
wind_class: {
value: getWeather.wind_class,
color: '#ff0033',
},
tem1: {
value: getWeather.tem1,
color: '#ff0000',
},
tem2: {
value: getWeather.tem2,
color: '#33ff33',
},
win: {
value: getWeather.win,
color: '#3399ff',
},
message: {
value: message,
color: '#8C8C8C',
},
};
...
}