Мало знаний, большой вызов! Эта статья участвует в "Необходимые знания для программистов«Творческая деятельность
Причина в том, что я смахнул точку кипения ежедневного сообщения-напоминания для своей девушки, и я автоматически отправляю сообщения каждый день. Это очень интересно. Я только недавно выучил яйца, и в нем есть задания на время, поэтому я решил попробовать реализация яйца.
Подготовка окружающей среды
Операционная система: поддержка macOS, Linux, Windows
Операционная среда: рекомендуется выбирать версию узла LTS, а минимальное требование — 8.x.
Введение в создание яичных проектов и структуру каталогов
бегать
местное развитие
$ 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');
}
}
сервисный сервисный уровень
// 时间处理
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;
}
Получить токен доступа
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: '*******',
};
Настройки особых моментов времени
Ниже день рождения, потому что мой родной город - лунный день рождения, с ним нелегко справиться, он временно записан на смерть
// 时间
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',
};
Конфигурация общедоступной учетной записи WeChat
Поскольку отдельные лица могут подать заявку только на учетную запись подписки, а учетная запись подписки не поддерживает отправку шаблонных сообщений, официальная учетная запись WeChat, используемая здесь, может подать заявку на любую учетную запись WeChat, без регистрации, отсканируйте код для входа.
Нет необходимости в общедоступной учетной записи, быстро подайте заявку на номер теста интерфейса
Непосредственно испытайте и протестируйте все расширенные интерфейсы общедоступной платформы.
// 测试 微信公众号
config.weChat = {
appld: '**********',
secret: '**********',
// 用户的openid
users: [
'**********************',
'**********************',
'**********************',
'**********************'
],
daily: '************', // 普通模板
marry: ''************',', // 结婚纪念日模板
wageDay: ''************',', // 工资日模板
birthday: ''************',', // 生日模板
};
Шаблон сообщения WeChat
Это необходимо установить отдельно в тестовой учетной записи общедоступной платформы WeChat, упомянутой выше.
Ниже приведен шаблон, который я использовал
обычный шаблон
{{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), и в настоящее время он изменен наОткрытая платформа карты BaiduAPI службы погоды для
Необходимые изменения
config.default.js
Бесплатный интерфейс Baidu Нет информации о качестве воздуха, я изменил шаблон, исходное качество воздуха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',
},
};
...
}