Создание приложения для голосования по SMS в реальном времени Часть 1: Node.js и CouchDB

Автор: | 22.07.2026

*Это первая часть многосерийного блога о создании приложения для голосования по SMS и голосу в реальном времени с использованием Node.js. В первой части мы создадим приложение Node.js, настроим базу данных CouchDB и подключим все к Twilio, чтобы мы могли обрабатывать голоса через SMS.*

Хотя я занимаюсь разработкой веб-приложений с 1999 года и использовал Java, PHP, Python и Ruby, это был мой первый опыт работы с такими технологиями, как Node.js и CouchDB. Во всех предыдущих проектах я использовал реляционную базу данных, такую как MySQL, как правило, со специфическим для языка ORM. Безболезненно переключившись между этими технологиями, я был шокирован тем, насколько растерянным я себя почувствовал, создавая приложение Node.js и работая с базой данных NoSQL, такой как CouchDB.

Я подумал, что может быть полезно для других людей, совершающих аналогичный переход, написать о своем опыте кодирования Votr, о подводных камнях, с которыми я столкнулся, и о нескольких найденных мною решениях. В этом первом посте я просто пройдусь по основам: созданию приложения, настройке базы данных и обработке голосов через SMS.

Во-первых, вам нужно будет установить Node.js и NPM. Для остальной части этого блога я буду работать в терминале Mac OS X, но эти шаги должны быть легко выполнимы в Windows или Linux. Вот как установить Express и создать приложение:

«`bash

npm install -g express

express votr

cd votr

npm install

«`

На данном этапе у вас должно быть стартовое приложение Node. Давайте развернем его в облаке.

Для целей этого руководства я буду использовать Nodejitsu для развертывания нашего приложения Node.

«`bash

npm install -g nodejitsu

jitsu config use

jitsu deploy

«`

Вас попросят ввести поддомен для вашего приложения и версию Node.js для использования. Значения по умолчанию должны работать нормально, так что не стесняйтесь нажимать Enter. После завершения развертывания проверьте, работает ли оно:

«`bash

jitsu ps

«`

Настройка локальной CouchDB проста, но для целей этого руководства я покажу вам, как использовать размещенную CouchDB от Cloudant. Я оценил несколько различных размещенных решений и столкнулся с наименьшим количеством проблем с Cloudant. Мне действительно понравился их веб-интерфейс для CouchDB.

Прежде чем приступить к этому, вы можете задаться вопросом, чем CouchDB (и NoSQL в целом) отличается от MySQL. Хотя это выходит за рамки этого руководства, ниже приведена таблица, сравнивающая некоторые основные концепции MySQL и CouchDB:

| MySQL | CouchDB |

|—|—|

| Таблицы | Коллекции |

| Строки | Документы |

| Столбцы | Поля |

| Схемы | Динамические схемы (функции валидации, но не схемы) |

В нашем приложении наши документы событий будут иметь следующую структуру:

**Name:** название события, по которому происходит голосование

**Short Name:** версия этого для URL-адреса

**State:** состояние голосования (включено, выключено и т. д.)

**Vote Options:** массив вариантов для голосования, где каждый вариант выглядит следующим образом

**Id:** номер для голосования

**Name:** имя для отображения

**Votes:** количество голосов, поданных за этот вариант

**Numbers:** массив телефонных номеров, проголосовавших за этот вариант

Давайте загрузим фиктивный документ события в нашу базу данных, чтобы мы могли приступить к созданию нашего приложения.

«`javascript

{

«_id»: «event_1»,

«name»: «Favorite Color»,

«short_name»: «color»,

«state»: «on»,

«vote_options»: [

{ «id»: 1, «name»: «Red», «votes»: 0, «numbers»: [] },

{ «id»: 2, «name»: «Blue», «votes»: 0, «numbers»: [] },

{ «id»: 3, «name»: «Green», «votes»: 0, «numbers»: [] }

]

}

«`

Как только наше приложение будет запущено и будет работать, нам нужно будет иметь возможность запрашивать события. В мире CouchDB для запроса документов используются представления (views). Для всех, кто работал с базами данных Oracle, это покажется знакомым. В Oracle представление — это просто предопределенный SQL-запрос, который ведет себя как таблица только для чтения. В CouchDB представление — это функция отображения (map function), которая обрабатывает все документы в вашей базе данных и создает хэш пар ключ/значение. Эти функции хранятся внутри специальных документов, называемых Design Documents. В нашем приложении мы захотим иметь возможность запрашивать события по их телефонному номеру и короткому имени. Давайте создадим документ дизайна и вставим содержимое Gist ниже.

«`javascript

{

«_id»: «_design/events»,

«views»: {

«by_phone»: {

«map»: «function (doc) { emit(doc.short_name, doc._id); }»

},

«by_short_name»: {

«map»: «function (doc) { emit(doc.short_name, doc._id); }»

}

}

}

«`

Итак, после закусок переходим к главному блюду: написанию нашего приложения Node.js. Команда express предоставила нам хорошую основу для использования, так что давайте начнем реализовывать конкретные детали.

«`javascript

// app.js

var express = require(‘express’);

var cradle = require(‘cradle’);

var twilio = require(‘twilio’);

var app = express.createServer();

// Modules

var votr = require(‘./votr’);

// Configuration

var config = require(‘./config’);

app.configure(function(){

app.set(‘views’, __dirname + ‘/views’);

app.set(‘view engine’, ‘ejs’);

app.use(express.bodyParser());

app.use(express.methodOverride());

app.use(app.router);

app.use(express.static(__dirname + ‘/public’));

});

// Routes

app.get(‘/’, function(req, res){

res.render(‘index’, {

locals: {

title: ‘Votr’

}

});

});

app.post(‘/sms’, function(req, res){

// Respond to Twilio

res.send(‘‘);

// Get vote details

var from = req.body.From;

var to = req.body.To;

var body = req.body.Body;

// Process vote

votr.processVote(from, to, body);

});

app.listen(8080);

«`

Создайте новый файл с именем `votr.js` в корневой папке вашего приложения. Этот файл будет содержать полезные функции, которые мы будем использовать в нашем приложении.

«`javascript

// votr.js

var cradle = require(‘cradle’);

var config = require(‘./config’);

var c = new cradle.Connection(config.couchdb.url, config.couchdb.port, {

auth: {

username: config.couchdb.username,

password: config.couchdb.password

}

});

var db = new Connection.database(‘votr’);

exports.processVote = function(from, to, body) {

db.get(‘event_1’, function (err, doc) {

if (err) {

console.log(‘Error getting event document: ‘ + err);

} else {

var response = votr.processVote(doc, from, body);

// Update document

db.save(response.doc, function (err, res) {

if (err) {

console.log(‘Error saving vote: ‘ + err);

} else {

console.log(‘Vote saved: ‘ + JSON.stringify(res));

}

});

}

});

}

exports.processVote = function(doc, from, body) {

var vote = parseInt(body);

var response = {

code: 200,

message: «»,

doc: doc

};

if (isNaN(vote)) {

response.message = «Invalid vote. Please send the number of the option you want to vote for.»;

} else {

var option = votr.getOption(doc, vote);

if (!option) {

response.message = «Invalid vote. That option does not exist.»;

} else {

if (votr.hasVoted(doc, from)) {

response.message = «You have already voted for this event.»;

} else {

// Cast vote

option.votes++;

option.numbers.push(from);

response.message = «Thank you for your vote!»;

}

}

}

return response;

}

exports.getOption = function(doc, vote) {

for (var i = 0; i < doc.vote_options.length; i++) { if (doc.vote_options[i].id == vote) { return doc.vote_options[i]; } } return false; } exports.findBy = function(key, value, callback) { db.view('events/by_short_name', { key: value, include_docs: true }, function (err, res) { if (err) { callback(err); } else { callback(false, res); } }); } exports.hasVoted = function(doc, phone_number) { for (var i = 0; i < doc.vote_options.length; i++) { for (var j = 0; j < doc.vote_options[i].numbers.length; j++) { if (doc.vote_options[i].numbers[j] == phone_number) { return true; } } } return false; } ``` Создайте новый файл с именем `config.js` в корневой папке вашего приложения. В этом файле мы будем хранить наши учетные данные для CouchDB и Twilio. ```javascript // config.js exports.couchdb = { url: 'https://your-username.cloudant.com', port: 443, username: 'your username', password: 'your password' }; exports.twilio = { sid: 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', token: 'your twilio auth token', number: '+15555555555' }; ``` Теперь, когда мы установили все наши зависимости, давайте запустим наше приложение, чтобы проверить, все ли работает: ```bash node app.js ``` Вы должны увидеть следующую ошибку, поскольку мы еще не установили наши пакеты: ```bash Error: Cannot find module 'cradle' ``` Это потому, что мы забыли установить наши зависимости! Давайте сделаем это сейчас: ```bash npm install cradle twilio ``` Теперь, когда мы добавили наши зависимости, давайте добавим их в наш файл манифеста пакета. ```json { "name": "votr", "version": "0.0.1", "dependencies": { "express": "2.5.8", "ejs": "0.8.3", "cradle": "0.6.6", "twilio": "1.0.0-rc.1" } } ``` Наконец, если вы хотите, чтобы ваше приложение работало в режиме реального времени, вы можете получить доступ к своим данным напрямую из CouchDB. В противном случае для работы этого приложения вам не нужно ничего делать. В продакшене вы, вероятно, захотите использовать представления CouchDB для доступа к своим данным. - config.couchdb.url: ‘https://your-username.cloudant.com’ - config.couchdb.port: 443 - config.couchdb.username: ‘your username’ - config.couchdb.password: ‘your password’ - findBy – ищет событие по ключу - hasVoted – проверяет, голосовал ли человек уже за конкретное событие - saveVote – сохраняет голос человека X-Twilio-Signature и отображает следующее видео для потенциальных злоумышленников. Хорошо, мы почти закончили! Последнее, что нам нужно сделать, это настроить номер телефона для обработки входящих голосов через SMS. Зайдите в свою учетную запись Twilio и перейдите в раздел «Numbers». Затем нажмите «Buy a Number» и найдите номер, поддерживающий SMS. Когда вы найдете подходящий номер, нажмите «Buy», затем «Configure». В разделе «Forwarding» выберите «Webhook» и введите URL вашего приложения. В нашем случае это будет `http://your-app-url.com/sms`. Когда вы введете свой номер, пожалуйста, убедитесь, что он в формате E.164 (например, +12026189023). На данном этапе вы можете взять свой телефон и начать тестировать свое приложение для голосования. Вот простой скрипт, который я использую: - Отправьте действительный голос: отправьте «1» (без кавычек) на номер голосования - Отправьте голос вне диапазона: отправьте «7» - Отправьте недействительный голос: отправьте «A» - Попробуйте проголосовать дважды: отправьте «1» Ниже вы увидите результаты этих текстовых сообщений в логах Nodejitsu для вашего приложения: ``` info: Received POST /sms info: Event: Favorite Color info: Voted: 1 info: Response: Thank you for your vote! ``` Давайте дважды проверим нашу базу данных в Cloudant, чтобы убедиться, что голос поступил: ![Cloudant Log](https://i.imgur.com/xxxxxxxx.png) На этом завершается первая часть создания приложения для голосования по SMS в реальном времени с использованием Node.js. Весь исходный код этого приложения можно найти на Github: https://github.com/crabasa/votr-part1. Вы, вероятно, задаетесь вопросом, где же часть про "реальное время". Во второй части мы создадим визуализацию для этого приложения для голосования с использованием Highcharts JS и анимируем эту визуализацию в реальном времени с помощью Socket.io.