Введение

Часто, глядя на повседневный мир, Инженеры иногда могут иметь ложное заблуждение, что проблемы не существует только потому, что решение кажется настолько очевидным, что они часто даже не задумываются о его ценности. Это общая черта в деловом мире и актуальность платформ SaaS. Причина просто в том, что часто люди, которые занимаются бизнесом, но не знают о возможностях технологий, могут видеть проблемы, которые могут показаться сложными им, но не бдительному инженеру.

Я столкнулся с этим из первых рук во время приключения от магазина к магазину, где я разговаривал с владельцами бизнеса на тему поисковой оптимизации, или, точнее, локального SEO. То, что я узнал, не только поразило меня, но и дало мне новый взгляд на эту тему. Первое, что я узнал, это то, что большинство владельцев малого бизнеса не подозревают о той силе, которую они имеют, чтобы продвигать свой бизнес вперед и доминировать в местных рейтингах. Для этого существует множество инструментов, одним из основных из которых является Google My Business.

Проблема

До моего взаимодействия с этими владельцами малого бизнеса я думал, что Google My Business полностью контролируется Google. Это было быстро исправлено.

Как оказалось, владельцы бизнеса имеют гораздо больше контроля над этим, чем я изначально предполагал. Одна из основных целей этого — позволить этим владельцам малого бизнеса обойти потребность в веб-сайте (для чего потребуется SEO) и вместо этого перечислить наиболее актуальную информацию. Это то, что появляется, когда я делаю простой запрос, такой как ниже:

В отличие от обычного веб-сайта, который требует поисковой оптимизации, для этого требуется локальная поисковая оптимизация. Статистика показывает, что большинство людей (около 60%) нажимают на первые три результата. Таким образом, любой бизнес, стремящийся найти клиентов, страдает из-за полного игнорирования этой функции в отношении Google. От многих из них я узнал, что просто проводить кампании Google Adwords или рекламу в Facebook недостаточно, а рентабельность инвестиций ничтожна. Таким образом, они часто полагались на эти возможности для привлечения клиентов и отчаянно нуждались в решении.

Итак, вопрос в том, что может помочь моему малому бизнесу попасть в первую тройку лидеров? Хотя есть много факторов, определяющих это, один из основных — Бизнес-отзывы. Это видно из скриншота выше. Итак, основной путь решения этой проблемы теперь ясен. Это можно выразить в постановке задачи.

Для владельца малого бизнеса O и клиента C, как я могу оптимизировать локальный SEO-рейтинг S, используя клиента C в диапазоне R при заданном времени T?

Решение должно состоять в том, чтобы повысить рейтинги бизнеса в течение 30 дней (поскольку рейтинги обновляются) для как можно большего числа клиентов. Это означает, что существующие клиенты будут фактором, используемым для получения новых клиентов, которые никогда не слышали о бизнесе. Google разрешает запрашивать отзывы, если не предусмотрено поощрение. Например, подарочная карта, скидки и т. д.

Решение: SkyRocket

Теперь мы собираемся создать MVP для решения этой проблемы. Основная цель MVP — быть максимально простым. Он должен ответить на вопрос; это действительно кому-то нужно? Таким образом, мы пока будем игнорировать причудливые фреймворки и рабочие процессы devops и вместо этого сосредоточимся на том, чтобы как можно скорее подготовить MVP. Он также должен быть построен менее чем за 5 часов.

Название SkyRocket было выбрано потому, что его основная цель — решить постановку задачи. А именно, получить более качественные отзывы о бизнесе за максимально короткое время в заданном диапазоне, чтобы увеличить количество клиентов. По сути, это надеется создать эффект маховика для бизнеса:

— Больше клиентов → Лучшие отзывы → Больше клиентов → Лучшие отзывы —

SkyRocket будет платформой SaaS и, по сути, предназначен для продажи бизнесу.

Абстракция

Суть проблемы заключается в том, чтобы привлечь клиентов, которые а) уже были в магазине раньше или б) сами нашли магазин, чтобы повысить отзывы и привлечь новых клиентов через Google.

  1. Покупатель посещает магазин. Это можно сделать разными способами. Например, если магазин является спа-салоном, стоматологией, парикмахерской, рестораном и т. д., клиент записывает на прием в приемной. Здесь они оставят соответствующую информацию, наиболее важной из которых является номер их активной ячейки. Будет сделано очевидным, что никто никогда не будет звонить и что с ними можно или нельзя будет связаться только один раз, после чего с ними больше никогда не свяжутся и они будут удалены из системы.
  2. Система автоматически обнаружит, когда клиент закончит с ними, и отправит автоматическое текстовое сообщение со ссылкой для просмотра их по номеру мобильного телефона. С обзором занять максимум 30 секунд.

Номер 2 очень важен, потому что 95% людей прочитают текстовое сообщение в течение первых 5 минут после его отправки, и если оно будет отправлено в течение небольшого промежутка времени после того, как у них была назначена встреча, у них будет больше шансов ответить. .

SkyRocket

Запись на прием осуществляется с помощью планшета на стойке регистрации/через форму на сайте.

Обмен текстовыми сообщениями будет выполняться автоматически путем заполнения пробелов соответствующей информацией из бэкэнда.

И это все!

В качестве бонусной функции, которая сегодня не будет рассматриваться, целевая страница Google My Business может быть оптимизирована с помощью независимых подрядчиков, привлеченных как на местном уровне, так и на таких сайтах, как Fiverr.

Лучший игрок

Для нашего MVP мы должны попытаться максимально минимизировать затраты (с точки зрения времени и денег) и, возможно, устранить их, если сможем. Для программного обмена текстовыми сообщениями мы собираемся использовать бесплатную пробную версию Twilio. Для внешнего интерфейса мы будем использовать базовый HTML, CSS, Vanilla Javascript и Bootstrap. Для серверной части мы можем использовать простой API Python Flask.

Мы можем построить внешний интерфейс как производный от нашего шаблона. В итоге я построил это:

Экран входа

Компания может войти в систему, используя свою учетную запись Gmail. Оттуда могут быть отправлены пользовательские инструкции для получения их информации Google My Business, такой как их ссылка для обзора и так далее.

Запись на прием

Это страница, которую клиенты могут использовать для записи на встречу с компанией. Поле даты указывает дату, в которую они желают встречи.

Экран текстовых сообщений

В конце дня, когда назначение было решено, в любое время с 16:00 до 21:00 SkyRocket отправит клиенту текстовое сообщение с просьбой о пересмотре. Поскольку поощрения не предусмотрено, запрос подпадает под действие закона.

Со всем этим MVP завершен.

Примечание

В целом проект занял ≤ 5 часов непрерывной работы, если вычесть перерывы. Он также содержит только необходимый код, необходимый для предоставления его потенциальным клиентам и сбора отзывов. Еще многое предстоит сделать, например, будущие TODO будут выглядеть примерно так:

  1. Перенесите API-интерфейс flask в Docker и разверните его на GCP.
  2. Используйте React для интерфейса
  3. Написать тесты для облачных функций Firebase
  4. Повторяйте отзывы пользователей, чтобы получить соответствие продукта рынку
  5. e.t.c

Таким образом, я сэкономил ресурсы с точки зрения времени и денег, чтобы получить базовый рабочий прототип.

Проект

Структура файла

Это файлы в каталоге SkyRocket:

Profile

Это указывает команду для запуска в контейнере heroku. Поскольку я запускаю фляжное приложение, следующий Procfile будет запускаться, когда я развертываю приложение на героку.

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

требования.txt

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

app.py

Это дом фляжного приложения. Это развернуто как отдельный API на героку. Облачная функция firebase, определенная в папке functions, вызывает этот API для отправки текстового сообщения с помощью twilio.

Для конфиденциальной информации используются переменные среды. Их можно установить в героку через командную строку, чтобы они сохранялись при удаленном развертывании, предпочтительно с помощью сценария развертывания. Если бы мы использовали docker, их можно было бы установить в файлах Dockerfile/dockerfile.yaml.

интерфейс.html

Это экран записи на прием

<!DOCTYPE html>
<html lang="en">
<head>
<title>Book An Appointment</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/paper.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/js/bootstrap.min.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-auth.js"></script>
<script>
var firebaseConfig = {
//get yours
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
var db = firebase.firestore();
</script>
</head>
<body>
Name:<br>
<input id="name" type="text" name="name">
<br>
Phone Number:<br>
<input type="text" name="phonenumber" id="phonenumber">
<br>
Cell Provider
<select id = "provider" name="cell provider">
<option value="@txt.bell.ca">Bell</option>
<option value="@fido.ca">Fido</option>
<option value="@txt.freedommobile.ca">Freedom</option>
</select>
<br>
Date:<br>
<input type="date" name="date" id="date">
<br>
<br>
Time:<br>
<input type="time" name="time" id="time">
<br>
<br><br>
<button onclick="bookAppointment()">Make An Appointment</button>
<button onclick="logout()">Logout</button>
</body>
<script>
var companyId = '';
window.id = '';
var user = firebase.auth().currentUser;
firebase.auth().onAuthStateChanged(function(user) {
if(user){
window.id = user.uid;
}else{
window.location.href = 'http://localhost:3000/login'
}
});
function logout(){
firebase.auth().signOut().then(function() {
console.log("Logged out")
}, function(error) {
alert("Error when logging out");
console.log(error);
});
}
function bookAppointment(){
var name = document.getElementById("name").value;
var phonenumber = document.getElementById("phonenumber").value;
var date = document.getElementById("date").value;
var time = document.getElementById("time").value;
var provider = document.getElementById("provider").value;
fetch('http://localhost:3000/bookAppointment?name='+name+'&phonenumber='+phonenumber+'&date='+date+'&time='+time+'&companyId='+window.id+'&provider='+provider)
.then(function(response) {
alert("Booking appointment")
})
.then(function(myJson) {
console.log(JSON.stringify(myJson));
}).catch(function(error){
console.log(error);
})
}
</script>
</html>

логин.html

Страница входа

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Pitter</title>
<script src="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.js"></script>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/paper.min.css">
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/3.5.2/firebaseui.css" />
</head>
<body>
<!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->
<!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-app.js"></script>
<!-- Add Firebase products that you want to use -->
<script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.2.4/firebase-firestore.js"></script>
<script>
var firebaseConfig = {
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
var db = firebase.firestore();
// Initialize the FirebaseUI Widget using Firebase.
var ui = new firebaseui.auth.AuthUI(firebase.auth());
var uiConfig = {
callbacks: {
signInSuccessWithAuthResult: function(authResult, redirectUrl) {
// User successfully signed in.
// Return type determines whether we continue the redirect automatically
// or whether we leave that to developer to handle.
return true;
},
uiShown: function() {
// The widget is rendered.
// Hide the loader.
document.getElementById('loader').style.display = 'none';
}
},
// Will use popup for IDP Providers sign-in flow instead of the default, redirect.
signInFlow: 'popup',
signInSuccessUrl: '',
signInOptions: [
// Leave the lines as is for the providers you want to offer your users.
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
],
// Terms of service url.
tosUrl: '<your-tos-url>',
// Privacy policy url.
privacyPolicyUrl: '<your-privacy-policy-url>'
};
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
window.location.href = 'http://localhost:3000/';
} else {
// No user is signed in.
console.log("User not signed in")
}
});
// The start method will wait until the DOM is loaded.
ui.start('#firebaseui-auth-container', uiConfig);
</script>
<!-- The surrounding HTML is left untouched by FirebaseUI.
Your app may use that space for branding, controls and other customizations.-->
<h1  style="text-align:center">SkyRocket</h1>
<div id="firebaseui-auth-container"></div>
<div id="loader">Loading...</div>
</body>
</html>

app.js

Мы запускаем интерфейс через локальный сервер. Однако позже это будет развернуто удаленно. Поскольку здесь мы получаем доступ к некоторым функциям Firebase, мы инициализируем приложение

var app = require('express')();
var server = require('http').Server(app);
var bodyParser = require('body-parser');
const admin = require('firebase-admin');
let serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
let db = admin.firestore();
server.listen(3000);
app.use(bodyParser.urlencoded({ extended: true }));
app.get('/', function (req, res) {
   res.sendFile(__dirname + '/frontend.html');
});
app.get('/login', function (req, res) {
res.sendFile(__dirname + '/login.html');
});
app.get('/bookAppointment', function(req, res) {
//add it to firebase database
var id = req.query["companyId"];
var phonenumber = req.query["phonenumber"];
var name = req.query["name"];
var date = req.query["date"];
var time = req.query["time"];
var provider = req.query["provider"];
var docRef = db.collection("appointments").doc(id);
docRef.get().then(function(doc) {
docRef.set({
id: id,
phonenumber: phonenumber,
name: name,
date: date,
time: time,
provider: provider
})
.then(function() {
console.log("Document successfully written!");
})
.catch(function(error) {
console.error("Error writing document: ", error);
});
}).catch(function(error) {
console.log("Error getting document:", error);
    });
});

функции/index.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const fetch = require('node-fetch');
var db = admin.firestore();
var request = require('request');
admin.initializeApp(functions.config().firebase);
//send message at 9:00pm end of day
exports.scheduledFunctionCrontab = functions.pubsub.schedule('00 21 * * *').timeZone('America/New_York').onRun((context) => {
var today = new Date();
var dd = String(today.getDate()).padStart(2, '0');
var mm = String(today.getMonth() + 1).padStart(2, '0');
var yyyy = today.getFullYear();
today = yyyy + '-' + mm + '-' + dd;
console.log(today)
let Ref = db.collection('appointments');
let query = Ref.where('date', '==', today).get()
.then(snapshot => {
if (snapshot.empty) {
console.log('No matching documents.');
return;
}
snapshot.forEach(doc => {
console.log(doc.data())
//Just a test link. We need real link from Google My Business
var link = 'http://search.google.com/local/writereview?placeid=ChIJw4GL3pIC0oURj5PoupsJdEA';
message = 'Hello '+doc.data().name+' thank you for visiting us recently. Could you please leave us a review at the link: '+link+' it will only take 30 seconds';
console.log(message)
return new Promise((resolve, reject) => {
request('https://skyrocket-reviews.herokuapp.com/getReview?message='+message+'&number='+doc.data().phonenumber+'&provider='+doc.data().provider, function (error, response, body) {
if (!error && response.statusCode == 200) {
console.log(response);
resolve();
}else{
console.log(error)
}
})
})
});
}).catch(err => {
console.log('Error getting documents', err);
});
});

Это все, что вам нужно для MVP. Дополнительные вещи, такие как настройка heroku, Twilio, биллинга GCP, функций firebase и flask, выходят за рамки этой статьи.

Следующие шаги

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

Я также хотел бы посмотреть, может ли это предложить действительное решение первоначальной постановки проблемы.

Проблемы/принятые решения

  • Одна из проблем, с которыми пришлось столкнуться, заключалась в том, следует ли использовать базу данных SQL или NoSQL. Для базы данных NoSQL я выбрал firebase, хотя изначально я начал с базы данных Postgres на моей машине VMWare (обратите внимание, что номер порта 5431 был изначально создан, а 5432 использовался только для этого снимка экрана)

Однако я сталкиваюсь с двумя серьезными конфликтами. Во-первых, смогу ли я спроектировать и внедрить базу данных вместе с оставшимся приложением в течение 5 часов, а второе связано с заданиями cron. Первоначально я планировал запустить задание cron в качестве облачной функции firebase, которая каждый день проверяла бы, было ли завершенное назначение обновлено запросом на проверку. Интеграция с облачным хранилищем Firebase была намного проще и даже быстрее. По этим причинам я решил придерживаться Firebase.

  • SMTP и Twilio. Изначально я хотел использовать SMTP вместо Twilio. Для этого нужно было внести несколько изменений в мою учетную запись Google, что сделало ее более уязвимой для угроз безопасности. Я сделал что-то подобное в приложении фляги:

Хотя он работал с локальным тестированием, на Heroku он отказывался проходить аутентификацию и требовал доступа к браузеру. Я попробовал несколько обходных путей, но не смог заставить его работать. Возможным решением было бы использовать Docker и аутентифицировать контейнер с помощью облачной оболочки, но соображения времени заставили меня вместо этого выбрать службу Twilio. Это, однако, остается в моей памяти для будущих реализаций.

Заключение

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