文章の目录
一、起因
在稀土掘金上刷到一篇文章【给女友写的,每日自动推送暖心消息】,每天自动定时发送消息,感觉很有趣,刚好最近在周会上听同事讲用 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
十、展示效果