SMS reminder app, Node/Twilio/Cron/Mongo: TypeError: Cannot read property 'utc' of undefined

SMS reminder app, Node/Twilio/Cron/Mongo: TypeError: Cannot read property 'utc' of undefined

我正在处理 Twilio 教程中的一些代码,一切似乎都运行良好,只是我没有收到任何通知。 Notifications Worker 运行后我收到此错误:

[grunt-develop] > (node:58755) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'utc' of undefined 

这是 appointment.js 文件,它似乎在 AppointmentSchema.methods 下导致错误:

 var mongoose = require('mongoose');
    var moment = require('moment');
    var twilio = require('twilio');



    var AppointmentSchema = new mongoose.Schema({
      phoneNumber: String,
      notification : String,
      timeZone: String,
      time : {type : Date, index : true}
    });




AppointmentSchema.methods.requiresNotification = function (date) {
      return Math.round(moment.duration(moment(this.time).tz(this.timeZone).utc()
                              .diff(moment(date).utc())
                            ).asMinutes()) === this.notification;
    };


    AppointmentSchema.statics.sendNotifications = function(callback) {

      // now
      var searchDate = new Date();
      Appointment
        .find()
        .then(function (appointments) {
          appointments = appointments.filter(function(appointment) {
                  return appointment.requiresNotification(searchDate);
          });
          if (appointments.length > 0) {
            sendNotifications(appointments);
          }
        });

        // Send messages to all appoinment owners via Twilio
        function sendNotifications(docs) {
            var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);
            docs.forEach(function(appointment) {
                // Create options to send the message
                var options = {
                    to: "+1" + appointment.phoneNumber,
                    from: '+17755834363',
                    body: "Just a reminder that you have an appointment coming up  " + moment(appointment.time).calendar() +"."
                };

                // Send the message!
                client.sendMessage(options, function(err, response) {
                    if (err) {
                        // Just log it for now
                        console.error(err);
                    } else {
                        // Log the last few digits of a phone number
                        var masked = appointment.phoneNumber.substr(0,
                            appointment.phoneNumber.length - 5);
                        masked += '*****';
                        console.log('Message sent to ' + masked);
                    }
                });
            });

            // Don't wait on success/failure, just indicate all messages have been
            // queued for delivery
            if (callback) {
              callback.call(this);
            }
        }
    };
    var Appointment = mongoose.model('appointment', AppointmentSchema);
    module.exports = Appointment;

我不知道为什么它未定义,数据库中的一切似乎都正常显示。而且我不知道这是否是导致实际上未发送通知的原因。我已经为此努力了一段时间,如果有人有任何见解那就太好了。

文件结构:

root
  ├ config
  | ├ auth.js
  | ├ database.js
  | ├ passport.js
  ├ controllers
  | ├appointments.js
  | ├routes.js
  ├ models
    ├appointment.js
    ├users.js
  ├ public
  ├ workers
    ├notificationsWorker
  ├ app.js
  ├ scheduler.js

通知的其他相关文件: appointments.js

var momentTimeZone = require('moment-timezone');
var Appointment = require('../models/appointment');
var moment = require('moment');

module.exports = function(app, client) {


app.post('/user', function(req, res, next) {
  var phoneNumber = req.body.phoneNumber;
  var notification = req.body.notification;
  var timeZone = req.body.timeZone;
  var time = moment(req.body.time, "MM-DD-YYYY hh:mma");

  var appointment = new Appointment({
    phoneNumber: phoneNumber,
    notification: notification,
    timeZone: timeZone,
    time: time
  });
  appointment.save()
    .then(function () {
      res.redirect('/user');
    });
});

notificationsWorker.js

var Appointment = require('../models/appointment')

var notificationWorkerFactory =  function(){
  return {
    run: function(){
      Appointment.sendNotifications();
    }
  };
};

module.exports = notificationWorkerFactory();

scheduler.js

var CronJob = require('cron').CronJob;
var notificationsWorker = require('./workers/notificationsWorker');
var moment = require('moment');

var schedulerFactory =  function(){
  return {
    start: function(){
      new CronJob('00 * * * * *', function() {
        console.log('Running Send Notifications Worker for ' +  moment().format());
        notificationsWorker.run();
      }, null, true, '');
    }
  };
};

module.exports = schedulerFactory();

最后 app.js 文件:

const dotenv = require('dotenv').config({path: '.env'});
const exp = require('express');
const bodyParser = require('body-parser'); //body parser
const methodOverride = require('method-override'); //method override
const app = exp();
const session = require('express-session');
const PORT = process.env.PORT || 3000;
const fetchUrl = require('fetch').fetchUrl;
const request = require('request');
const sass = require('node-sass');
const exphbs = require('express-handlebars');
const favicon = require('serve-favicon');
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');

var appointments = require('./controllers/appointments');
var scheduler = require('./scheduler');


var ACCOUNTSID = process.env.TWILIO_ACCOUNT_ID;
var AUTHTOKEN = process.env.TWILIO_AUTH_TOKEN;

var twilio = require('twilio');
var client = new twilio.RestClient(ACCOUNTSID, AUTHTOKEN);


//databse stuff
const db = require('./config/database.js');
// mongoose.connect(db.url); // connect to our database

//passport
const passport = require('passport');
const flash    = require('connect-flash');
app.use(session({ secret: 'blahblahblahbleck' })); // session secret
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use connect-flash for flash messages stored in session

//views/middleware configs
app.engine('handlebars', exphbs({
  layoutsDir: __dirname + '/views/layouts/',
  defaultLayout: 'main',
  partialsDir: [__dirname + '/views/partials/']
}));
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/views');
app.locals.pretty = true
app.use('/', exp.static(__dirname + '/public'));
app.use('/bower_components',  exp.static(__dirname + '/bower_components'));
app.use(methodOverride('_method')) //method override
app.use(bodyParser.urlencoded({
  extended: false
  // app.use(favicon(__dirname + '/public/imgages/favicon.ico'));
})); //body parser
app.use(bodyParser.json()); //body parser
app.use(cookieParser());
app.use(morgan('dev'));
app.locals.moment = require('moment');

require('./config/passport')(passport);
require('./controllers/routes')(app, passport);
require('./controllers/appointments')(app, client, db);

app.use('./controllers/appointments', appointments);
app.use('/', appointments);


// dynamically set controllers(routes)
fs.readdirSync('./controllers').forEach(function(file) {
    routes = require('./controllers/' + file);
});

//start the server
app.listen(PORT, function() {
  console.log('things that make you go hmmm on port ' + PORT);
});

scheduler.start();

module.exports = app;

==================已更新=================

我的表单输入视图:

 <form class="omb_loginForm" action="/" autocomplete="off" method="POST">

  <span class="help-block"></span>
  <div class="input-group">
    <span class="input-group-addon"></span>
    <input type="text" class="form-control" name="phoneNumber" placeholder="phone number">
  </div>

  <span class="help-block"></span>

  <div class="input-group">
    <span class="input-group-addon"></span>
    <input type="text" class="form-control" name="notification" placeholder="notification">
  </div>

  <span class="help-block"></span>

  <div class="input-group">
    <span class="input-group-addon"></span>
    <select class="form-control" name="timeZone">
      {{#each timeZone}}
      <option>{{this}}</option>
      {{/each}}
    </select>
  </div>

  <span class="help-block"></span>

   <div class="input-group date" id="datetimepicker1">
    <input class="form-control" name="time">
    <span class="input-group-addon glyphicon-calendar glyphicon">
    </span>
  </div>

  <span class="help-block"></span>


  <button class="btn btn-lg btn-primary btn-block" type="submit">
  Submit
  </button>
</form>

原始表单视图:

.form-group
  label.col-sm-4.control-label(for='inputName') Name *
  .col-sm-8
    input#inputName.form-control(type='text', name='name', placeholder='Name', required='', data-parsley-maxlength='20', data-parsley-maxlength-message="This field can't have more than 20 characters", value="#{appointment.name}")
.form-group
  label.col-sm-4.control-label(for='inputPhoneNumber') Phone Number
  .col-sm-8
    input#inputPhoneNumber.form-control(type='number', name='phoneNumber', placeholder='Phone Number', required='', value="#{appointment.phoneNumber}")
.form-group
  label.col-sm-4.control-label(for='time') Appointment Date
  .col-sm-8
    input#inputDate.form-control(type='text', name='time', placeholder='Pick a Date', required='', value="#{moment(appointment.time).format('MM-DD-YYYY hh:mma')}")
.form-group
  label.col-sm-4.control-label(for='selectNotification') Notification Time
  .col-sm-8
    select#selectDelta.form-control(name='notification', required='', value="#{appointment.notification}")
      option(selected=appointment.notification == '', value='') Select a time
      option(selected=appointment.notification == '15', value='15') 15 Minutes
      option(selected=appointment.notification == '30', value='30') 30 Minutes
      option(selected=appointment.notification == '45', value='45') 45 Minutes
      option(selected=appointment.notification == '60', value='60') 60 Minutes
.form-group
  label.col-sm-4.control-label(for='selectTimeZone') Time Zone
  .col-sm-8
    select#selectTimeZone.form-control(name='timeZone', required='', value="#{appointment.timeZone}")
      each zone in timeZones
        option()
option(selected=zone == appointment.timeZone, value="#{zone}") !{zone}

routes.js

app.get('/user', isLoggedIn, function(req, res) {
          res.render('user', {
              user : req.user,
              timeZone: timeZones(),
              appointment : new Appointment({
                phoneNumber: "",
                notification: '',
                timeZone: "",
                time:''}),
              loggedIn: true, // get the user out of session and pass to template
              layout: 'home'
          });
      });

通过更改 requiresNotification 中的逻辑,我终于能够让它工作。这是更新后的代码:

AppointmentSchema.methods.requiresNotification = function (date) {
  var apptDate = moment.utc(this.time);
  var current = moment.utc(date);
  return Math.round(moment.duration(current.diff(apptDate))
    .asMinutes()) === 0;
};

我正在查找约会时间和当前时间之间的时差。所以现在当 appointment date/time 和 current date/time 的分钟差为 0 时,发送通知.