给女友写的,每日自动推送暖心消息

23,495 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

起因是因为刷到一则给女友发的每日提醒消息的沸点,每天自动定时发送消息,感觉很有趣,刚好最近在学习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

image

配置文件说明

└── 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}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。

展示效果

d086745c18c811ef17488c004f64cb0.jpg

81c079890d8acd86cf02aeb22c1ff4b.jpg

0fb7faaa548c212ae6c31bf5d9ce816.jpg


天气接口 修改

上面的提到的接口已经不能用(小公司就是不靠谱 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',
        },
      };
      ...
  }