一、起因

在稀土掘金上刷到一篇文章【给女友写的,每日自动推送暖心消息】,每天自动定时发送消息,感觉很有趣,刚好最近在周会上听同事讲用 nodejs框架实现前后端考试系统,自己也通过百度了解到nodejs后端框架排名如图,于是决定尝试一把 nestjs(nodejs框架排第二名) 实现。

给老婆写的,每日自动推送暖心消息-LMLPHP

二、环境准备

操作系统:windows、Linux、MacOs
运行环境:Nodejs>=12 v13版本除外

三、创建nestjs项目

使用 Nest CLI 建立新项目非常简单。 在安装好 npm 后,您可以使用下面命令在您的 OS 终端中创建 Nest 项目:

$ npm i -g @nestjs/cli
$ nest new project-name

将会创建 project-name 目录, 安装 node_modules 和一些其他样板文件,并将创建一个 src 目录,目录中包含几个核心文件。

src
 ├── app.controller.spec.ts
 ├── app.controller.ts
 ├── app.module.ts
 ├── app.service.ts
 └── main.ts

以下是这些核心文件的简要概述:

给老婆写的,每日自动推送暖心消息-LMLPHP
main.ts 包含一个异步函数,它负责引导我们的应用程序:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

要创建一个 Nest 应用实例,我们使用了 NestFactory 核心类。NestFactory 暴露了一些静态方法用于创建应用实例。 create() 方法返回一个实现 INestApplication 接口的对象。该对象提供了一组可用的方法。 在上面的 main.ts 示例中,我们只是启动 HTTP 服务,让应用程序等待 HTTP 请求。

四、控制器

@Controller('ymn')
export class YmnController {
  constructor(private ymnService: YmnService) {}

  @Get()
  async send(@Res() response: Response) {
    const result = await this.ymnService.sendOut();
    response.status(HttpStatus.OK).send('ok');
  }
}

五、service服务层

// 时间处理
const moment = require('moment');

@Injectable()
export class YmnService {
  constructor(
    private configService: ConfigService,
    private readonly httpService: HttpService,
  ) {}

  /**
   * @method 发送模板消息给媳妇儿
   * @returns any[]
   */
  async sendOut(): Promise<any[]> {
    const token = await this.getToken();
    const data = await this.getTemplateData();
    console.log(data);
    // 模板消息接口文档
    const users = this.configService.get('weixin.users');
    const promise = users.map((id) => {
      data.touser = id;
      return this.toWechart(token, data);
    });
    const results = await Promise.all(promise);
    return results;
  }

  /**
   * @method 获取token
   * @returns string token
   */
  async getToken(): Promise<string> {
    ...
  }

  /**
   * @method 组装模板消息数据
   * @returns dataType 组装完的数据
   */
  async getTemplateData(): Promise<dataType> {
    ...
  }

  /**
   * @method 获取距离下次发工资还有多少天
   * @returns number
   */
  getWageDay(): number {
    ...
  }

  /**
   * @method 获取距离下次结婚纪念日还有多少天
   * @returns number
   */
  getWeddingDay(): number {
    ...
  }

  /**
   * @method 获取距离下次生日还有多少天
   * @returns number
   */
  getbirthday(): number {
    ...
  }

  /**
   * @method 获取时间日期
   * @returns string
   */
  getDatetime(): string {
    ...
  }

  /**
   * @method 获取是第几个结婚纪念日
   * @returns number
   */
  getMarryYear(): number {
    ...
  }

  /**
   * @method 获取相恋几年了
   * @returns number
   */
  getLoveYear(): number {
    ...
  }

  /**
   * @method 获取是第几个生日
   * @returns number
   */
  getBirthYear(): number {
    ...
  }

  /**
   * @method 获取天气
   * @returns
   */
  async getWeather(): Promise<weatherType> {
    ...
  }

  /**
   * @method 获取每日一句
   * @returns string
   */
  async getOneSentence(): Promise<string> {
    ...
  }

  /**
   * @method 获取相恋天数
   * @returns number
   */
  getLoveDay(): number {
    ...
  }

  /**
   * @method 通知微信接口
   * @param token
   * @param data
   * @returns
   */
  async toWechart(token: string, data: dataType): Promise<any> {
    ...
  }
}

1、获取Access token

  /**
   * @method 获取token
   * @returns string token
   */
  async getToken(): Promise<string> {
    const grantType = this.configService.get('weixin.grant_type');
    const appid = this.configService.get('weixin.appid');
    const secret = this.configService.get('weixin.secret');
    // 模板消息接口文档
    const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=${grantType}&appid=${appid}&secret=${secret}`;
    const result = await firstValueFrom(this.httpService.get(url));
    if (result.status === 200) return result.data.access_token as string;
    else return '';
  }

2、组装模板消息数据

  /**
   * @method 组装模板消息数据
   * @returns dataType 组装完的数据
   */
  async getTemplateData(): Promise<dataType> {
    // 判断所需模板
    // 发工资模板 getWageDay === 0       wageDay
    // 结婚纪念日模板 getWeddingDay === 0  weddingDay
    // 生日模板 getbirthday === 0       birthday
    // 正常模板                         daily
    const wageDay = this.getWageDay();
    const weddingDay = this.getWeddingDay();
    const birthday = this.getbirthday();
    const data = {
      topcolor: '#FF0000',
      template_id: '',
      data: {},
      touser: '',
    };
    // 发工资模板
    if (!wageDay) {
      data.template_id = this.configService.get('weixin.wage_day');
      data.data = {
        dateTime: {
          value: this.getDatetime(),
          color: '#cc33cc',
        },
      };
    } else if (!weddingDay) {
      // 结婚纪念日模板
      data.template_id = this.configService.get('weixin.wedding_day');
      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 = this.configService.get('weixin.birthdate');
      data.data = {
        dateTime: {
          value: this.getDatetime(),
          color: '#cc33cc',
        },
        individual: {
          value: this.getBirthYear(),
          color: '#ff3399',
        },
      };
    } else {
      // 正常模板
      data.template_id = this.configService.get('weixin.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: weddingDay,
          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',
        },
      };
    }
    return data;
  }

3、获取下次发工资还有多少天

  /**
   * @method 获取距离下次发工资还有多少天
   * @returns number
   */
  getWageDay(): number {
    const wageDay = this.configService.get('time.wage_day');
    // 获取日期 day
    // 如果在wage号之前或等于wage时,那么就用wage-day
    // 如果在wage号之后,那么就用wage +(当前月总天数 - day)
    // 当日日期day
    const day = moment().date();
    // 当月总天数
    const nowDayTotal = moment().daysInMonth();
    // // 下个月总天数
    let resultDay = 0;
    if (day <= wageDay) resultDay = wageDay - day;
    else resultDay = wageDay + (nowDayTotal - day);
    return resultDay;
  }

4、获取距离下次结婚纪念日还有多少天

  /**
   * @method 获取距离下次结婚纪念日还有多少天
   * @returns number
   */
  getWeddingDay(): number {
    const weddingDay = this.configService.get('time.wedding_day');
    // 获取当前时间戳
    const now = moment(moment().format('YYYY-MM-DD')).valueOf();
    // 获取纪念日 月-日
    const mmdd = moment(weddingDay).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',
    );
  }

5、获取距离下次生日还有多少天

  /**
   * @method 获取距离下次生日还有多少天
   * @returns number
   */
  getbirthday(): number {
    const birthdate = this.configService.get('time.birthdate');
    // 获取当前时间戳
    const now = moment(moment().format('YYYY-MM-DD')).valueOf();
    // 获取当年
    const y = moment().year();
    // 获取纪念日 月
    const m = moment(birthdate).month();
    // 获取纪念日 日
    const d = moment(birthdate).date();
    // 获取今年生日阳历
    const nowBirthday = lunisolar
      .fromLunar({
        year: y,
        month: m + 1,
        day: d,
      })
      .format('YYYY-MM-DD');
    // 获取今年生日时间戳
    const nowTimeNumber = moment(nowBirthday).valueOf();
    // 判断生日有没有过,如果已经过去(now>nowTimeNumber),resultBirthday日期为明年的生日日期
    // 如果还没到,则结束日期为今年的目标日期
    let resultBirthday = nowTimeNumber;
    if (now > nowTimeNumber) {
      // 获取明年生日阳历
      const nextBirthday = lunisolar
        .fromLunar({
          year: y + 1,
          month: m + 1,
          day: d,
        })
        .format('YYYY-MM-DD');
      // 获取明年目标日期
      resultBirthday = moment(nextBirthday).valueOf();
    }
    return moment(moment(resultBirthday).format()).diff(
      moment(now).format(),
      'day',
    );
  }

6、获取时间日期

  /**
   * @method 获取时间日期
   * @returns string
   */
  getDatetime(): string {
    const week = {
      1: '星期一',
      2: '星期二',
      3: '星期三',
      4: '星期四',
      5: '星期五',
      6: '星期六',
      0: '星期日',
    };
    return moment().format('YYYY年MM月DD日 ') + week[moment().weekday()];
  }

7、获取是第几个结婚纪念日

  /**
   * @method 获取是第几个结婚纪念日
   * @returns number
   */
  getMarryYear(): number {
    const weddingDay = this.configService.get('time.wedding_day');
    return moment().year() - moment(weddingDay).year();
  }

8、获取相恋几年了

  /**
   * @method 获取相恋几年了
   * @returns number
   */
  getLoveYear(): number {
    const loveDay = this.configService.get('time.love');
    return moment().year() - moment(loveDay).year();
  }

9、获取是第几个生日

  /**
   * @method 获取是第几个生日
   * @returns number
   */
  getBirthYear(): number {
    const birthdate = this.configService.get('time.birthdate');
    return moment().year() - moment(birthdate).year();
  }

10、获取天气

  /**
   * @method 获取天气
   * @returns
   */
  async getWeather(): Promise<weatherType> {
    try {
      const dataType = this.configService.get('weather.data_type');
      const ak = this.configService.get('weather.ak');
      const districtId = this.configService.get('weather.district_id');
      // https://api.map.baidu.com/weather/v1/?district_id=130128&data_type=all&ak=bGjmaBLLzlBZXTiAkOwSqiVjftZlg17O
      const url = `https://api.map.baidu.com/weather/v1/?district_id=${districtId}&data_type=${dataType}&ak=${ak}`;
      const result: any = await firstValueFrom(this.httpService.get(url));
      // "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: '未知',
      };
    }
  }

11、获取每日一句

  /**
   * @method 获取每日一句
   * @returns string
   */
  async getOneSentence(): Promise<string> {
    const url = 'https://v1.hitokoto.cn/';
    const result = await firstValueFrom(this.httpService.get(url));
    if (result && result.status === 200) return result.data.hitokoto;
    else return '今日只有我爱你!';
  }

12、获取相恋天数

  /**
   * @method 获取相恋天数
   * @returns number
   */
  getLoveDay(): number {
    const loveDay = this.configService.get('time.love');
    return moment(moment().format('YYYY-MM-DD')).diff(loveDay, 'day');
  }

13、发送模板消息

  /**
   * @method 通知微信接口
   * @param token
   * @param data
   * @returns
   */
  async toWechart(token: string, data: dataType): Promise<any> {
    // 模板消息接口文档
    const url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`;
    const result = await firstValueFrom(this.httpService.post(url, data));
    return result;
  }

六、定义的数据类型

interface dataItemType {
  value: string;
  color: string;
}

export interface dataType {
  topcolor: string;
  template_id: string;
  touser: string;
  data: Record<any, dataItemType>;
}

export interface weatherType {
  wea: string;
  tem: string;
  tem1: string;
  tem2: string;
  win: string;
  wind_class: string;
}

七、配置文件

配置方法参考配置

1、微信公众号配置

因个人只能申请订阅号,而订阅号不支持发送模板消息,所以在此使用的测试的微信公众号,有微信号都可以申请,免注册,扫码登录

无需公众帐号、快速申请接口测试号

直接体验和测试公众平台所有高级接口

申请地址

import { registerAs } from '@nestjs/config';

export default registerAs('weixin', () => ({
  grant_type: '*',
  appid: '*',
  secret: '*',
  users: ['*'],// 用户的openid
  wage_day: '*',// 工资日模板
  wedding_day: '*',// 结婚纪念日模板
  birthdate: '*',// 生日模板
  daily: '*',// 普通模板
}));

2、特殊时间点设置

import { registerAs } from '@nestjs/config';

// 时间
export default registerAs('time', () => ({
  wage_day: 10, // 工资日
  wedding_day: '*',// 结婚纪念日
  birthdate: '*',// 生日阴历
  love: '*',// 相爱日期
}));

3、天气配置

import { registerAs } from '@nestjs/config';

export default registerAs('weather', () => ({
  data_type: 'all',
  ak: '*',
  district_id: '110100',
}));

八、微信消息模板

这个需要在上文提到的 微信公众平台测试账号 单独设置

以下是我用的模板

1、正常模板

{{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}}

2、发工资模板

{{dateTime.DATA}}
老婆大人,今天要发工资了,预计晚九点前会准时上交,记得查收!

3、生日模板

{{dateTime.DATA}}
听说今天是你人生当中第 {{individual.DATA}} 个生日?天呐,
我差点忘记!因为岁月没有在你脸上留下任何痕迹。
尽管,日历告诉我:你又涨了一岁,但你还是那个天真可爱的小妖女,生日快乐!

4、结婚纪念日

{{dateTime.DATA}}
今天是结婚{{anniversary.DATA}}周年纪念日,在一起{{year.DATA}}年了,
经历了风风雨雨,最终依然走在一起,很幸运,很幸福!我们的小家庭要一直幸福下去。

九、本地开发

$ yarn
$ yarn start:dev

十、展示效果

给老婆写的,每日自动推送暖心消息-LMLPHP
给老婆写的,每日自动推送暖心消息-LMLPHP
给老婆写的,每日自动推送暖心消息-LMLPHP
给老婆写的,每日自动推送暖心消息-LMLPHP

参考

  1. node.js 后端框架star 排名 2023年2月更新
  2. 给女友写的,每日自动推送暖心消息

写在最后

09-11 03:38