文章の目录
- 一、起因
 - 二、环境准备
 - 三、创建nestjs项目
 - 四、控制器
 - 五、service服务层
 - 1、获取Access token
 - 2、组装模板消息数据
 - 3、获取下次发工资还有多少天
 - 4、获取距离下次结婚纪念日还有多少天
 - 5、获取距离下次生日还有多少天
 - 6、获取时间日期
 - 7、获取是第几个结婚纪念日
 - 8、获取相恋几年了
 - 9、获取是第几个生日
 - 10、获取天气
 - 11、获取每日一句
 - 12、获取相恋天数
 - 13、发送模板消息
 
- 六、定义的数据类型
 - 七、配置文件
 - 1、微信公众号配置
 - 2、特殊时间点设置
 - 3、天气配置
 
- 八、微信消息模板
 - 1、正常模板
 - 2、发工资模板
 - 3、生日模板
 - 4、结婚纪念日
 
- 九、本地开发
 - 十、展示效果
 - 参考
 - 写在最后
 
一、起因
在稀土掘金上刷到一篇文章【给女友写的,每日自动推送暖心消息】,每天自动定时发送消息,感觉很有趣,刚好最近在周会上听同事讲用 nodejs框架实现前后端考试系统,自己也通过百度了解到nodejs后端框架排名如图,于是决定尝试一把 nestjs(nodejs框架排第二名) 实现。

二、环境准备
操作系统: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
 
以下是这些核心文件的简要概述:

 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
 
十、展示效果

 
 
 
参考
- node.js 后端框架star 排名 2023年2月更新
 - 给女友写的,每日自动推送暖心消息
 
写在最后
如果你感觉文章不咋地
//(ㄒoㄒ)//,就在评论处留言,作者继续改进;o_O???
如果你觉得该文章有一点点用处,可以给作者点个赞;\\*^o^*//
如果你想要和作者一起进步,可以微信扫描二维码,关注前端老L;~~~///(^v^)\\\~~~
谢谢各位读者们啦(^_^)∠※!!!













![Java中如何判断字符串输入[hello,world]是不是字符串数组参数](https://img-blog.csdnimg.cn/7098aae56e554bd18d8dba7c78d5c4a6.png#pic_center)




