Введение
Часто, глядя на повседневный мир, Инженеры иногда могут иметь ложное заблуждение, что проблемы не существует только потому, что решение кажется настолько очевидным, что они часто даже не задумываются о его ценности. Это общая черта в деловом мире и актуальность платформ 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.
- Покупатель посещает магазин. Это можно сделать разными способами. Например, если магазин является спа-салоном, стоматологией, парикмахерской, рестораном и т. д., клиент записывает на прием в приемной. Здесь они оставят соответствующую информацию, наиболее важной из которых является номер их активной ячейки. Будет сделано очевидным, что никто никогда не будет звонить и что с ними можно или нельзя будет связаться только один раз, после чего с ними больше никогда не свяжутся и они будут удалены из системы.
- Система автоматически обнаружит, когда клиент закончит с ними, и отправит автоматическое текстовое сообщение со ссылкой для просмотра их по номеру мобильного телефона. С обзором занять максимум 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 будут выглядеть примерно так:
- Перенесите API-интерфейс flask в Docker и разверните его на GCP.
- Используйте React для интерфейса
- Написать тесты для облачных функций Firebase
- Повторяйте отзывы пользователей, чтобы получить соответствие продукта рынку
- 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-часовая реализация простой идеи. Я надеюсь, что со временем это может стать лучше, чем оно есть, поскольку оно решает реальную проблему. Сегодняшним владельцам бизнеса не хватает навыков, необходимых для оцифровки их бизнеса в мире, который все глубже погружается в компьютерную эру. Таким образом, крайне важно, чтобы те, у кого есть навыки, чтобы помочь им, постоянно обращались к ним и находили проблемы в реальном мире, которые стоит решить.