Коллегам в отдел маркетинга прислали задачу: получить как можно больше подписчиков в рассылку, не прибегая к использованию готовых программ, но при этом выделиться на фоне конкурентов. На ум приходит размещение яркого рекламного предложения-плаката с призывом подключиться к рассылке. Попробуем его оформить, сгенерировать QR-код и настроить передачу данных в CRM.

Пусть пользователь прочитает содержимое предложения, решит, что оно ему подходит, а что дальше? Заставить проходить алгоритм «Напиши тому, настрой то-то, зарегистрируйся там-то»? Слишком долго — мало желающих тратить свое время.

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

  1. Сгенерировать единый QR-код, который будет вести на форму отправки сообщения на номер МТС Exolve.

  2. С помощью подручных средств создать предложение-плакат с реальным бонусом за переход по QR-коду (нужно позаботиться о тексте соглашения на рассылку на плакате).

  3. После отправки сообщения (в котором вы можете попросить написать что угодно) внутри Exolve будет определяться номер телефона отправителя.

  4. С помощью API Exolve собрать все сообщения в одном месте — для этого есть специальная функция, но внутри нее нужно настроить фильтрацию только по номерам.

  5. Передать собранные номера телефонов в единую базу CRM или Excel-файл.

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

Какие есть сложности

Несмотря на то, что алгоритм сам по себе выглядит отлаженным на бумаге, на практике столкнулись с некоторыми сложностями — расскажем о них заранее:

  • Разве это выгодно с экономической точки зрения? Будут ли пользователи отправлять сообщения (возможно, платные) на неизвестные номера?

Если работать как ИП или ООО и пройти соответствующую проверку в Exolve, можно создать имя отправителя, которое будет высвечиваться вместо номера телефона. Большая часть современных тарифов операторов подразумевает наличие бесплатных SMS-сообщений, но также никто не мешает настроить отправку через онлайн-сервисы. Для мотивации выполнения целевого действия подключается лид-магнит.

  • Какие требования выдвигаются к серверу?

Из основных — go 1.20, SSL (https), маршрутизация (виртуальный хост, например nginx). Все остальное — на усмотрение компании, в соответствии с масштабом входящих запросов.

  • Насколько дорого обойдется поддержка данного метода?

В силу того, что мы даем готовый код для работы на языке GO, может потребоваться помощь программиста для его настройки под свои нужды (подключение к другой CRM, добавление смежных функций). Также потребуется содержать сервер (виртуальный или физический) и взаимодействовать с системой MTC Exolve.

Часть 1. Генерация QR-кода

Хоть в интернете достаточно программ для создания QR-кодов, мы реализовали собственную — для того, чтобы максимально удобно и без монетизации создавать привязку к номеру телефона:

package qr
import (
	"encoding/json"
	"fmt"
	"github.com/skip2/go-qrcode"
	"io"
	"log"
	"net/http"
	"os"
	"qr/exolve"
	"qr/exolve/models"
	"strconv"
)
func CreateQR() {
	sendSms := `smsto:”Номер телефона Exolve”`
	var size, content string = "256", sendSms
	var codeData []byte
	qrCodeSize, err := strconv.Atoi(size)
	qrCode := simpleQRCode{Content: content, Size: qrCodeSize}
	codeData, err = qrCode.Generate()
	if err != nil {
		log.Println("generate code error: ", err)
	}
	qrCodeFileName := "qrcode.png"
	qrCodeFile, err := os.Create(qrCodeFileName)
	if err != nil {
		log.Println("create file err: ", err)
	}
	defer qrCodeFile.Close()
	qrCodeFile.Write(codeData)
}
func HandleRequest(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(10 << 20)
	sendSms := `smsto:+79587329155`
	var size, content string = "256", sendSms
	var codeData []byte
	w.Header().Set("Content-Type", "application/json")
	if content == "" {
		w.WriteHeader(400)
		json.NewEncoder(w).Encode(
			"Could not determine the desired QR code content.",
		)
		return
	}
	qrCodeSize, err := strconv.Atoi(size)
	if err != nil || size == "" {
		w.WriteHeader(400)
		json.NewEncoder(w).Encode("Could not determine the desired QR code size.")
		return
	}
	qrCode := simpleQRCode{Content: content, Size: qrCodeSize}
	codeData, err = qrCode.Generate()
	if err != nil {
		w.WriteHeader(400)
		json.NewEncoder(w).Encode(
			fmt.Sprintf("Could not generate QR code. %v", err),
		)
		return
	}
	w.Header().Set("Content-Type", "image/png")

	// Save the QR code as a .png file
	qrCodeFileName := "qrcode.png"
	qrCodeFile, err := os.Create(qrCodeFileName)
	if err != nil {
		w.WriteHeader(500)
		w.Write([]byte("Failed to create QR code file"))
		return
	}
	defer qrCodeFile.Close()
	qrCodeFile.Write(codeData)
	// Now, the QR code is saved as "qrcode.png" in the current working directory
	w.Write(codeData)
	var message models.Message
	if r.Method == "POST" {
		bs, _ := io.ReadAll(r.Body)
		if bs != nil {
			//exolve.GetCount()
			exolve.GetList()
			w.Write(bs)
			w.WriteHeader(200)
			jsonData := json.Unmarshal(bs, &message)
			if jsonData != nil {
				fmt.Println("Message received successfully")
			}
			log.Println("response:", message)
		}
		log.Println("resp:", string(bs))
	}
}
type simpleQRCode struct {
	Content string
	Size    int
}
func (code *simpleQRCode) Generate() ([]byte, error) {
	qrCode, err := qrcode.Encode(code.Content, qrcode.Medium, code.Size)
	if err != nil {
		return nil, fmt.Errorf("could not generate a QR code: %v", err)
	}
	return qrCode, nil
}

В этой части кода происходит генерация QR-кода (необходимо изменить номер телефона на актуальный, чтобы все работало). В итоге генерируется соответствующее изображение формата png — «qrcode.png». Перейдя по нему с помощью камеры, пользователь попадет в приложение отправки SMS с заранее выбранным номером телефона.

Вторая функция HandleRequest отвечает за обработку запроса — можно ли создать QR-код в целом, и, если нельзя — почему. Помимо того, что происходит проверка на основные проблемы (пустая строка, неверное значение), выводится соответствующее сообщение-предупреждение.

В процессе работы с QR-кодом и последующими данными потребуется несколько дополнительных функций:

package qr
import (
	"fmt"
	"os"
	"regexp"
	"strings"
)
func copyFile(src, dst string) error {
	source, err := os.ReadFile(src)
	if err != nil {
		return err
	}
	err = os.WriteFile(dst, source, 0644)
	if err != nil {
		return err
	}
	return nil
}
func generateQRCode(content, filename string) error {
	qrCodeSize := 256 // Set your desired QR code size
	qrCode := simpleQRCode{Content: content, Size: qrCodeSize}
	codeData, err := qrCode.Generate()
	if err != nil {
		return err
	}
	// Save the QR code as the specified filename
	err = os.WriteFile(filename, codeData, 0644)
	if err != nil {
		return err
	}
	return nil
}
func FindFile(substr string) string {
	var substring string
	file, err := os.ReadFile("settings")
	if err != nil {
		fmt.Println("Error reading")
	}
	list := strings.Split(string(file), ",")
	for k, _ := range list {
		withoutSpaces := strings.Join(strings.Fields(list[k]), "")
		if strings.Contains(withoutSpaces, substr) {
			pattern := `<([^>]+)>`
			re := regexp.MustCompile(pattern)
			match := re.FindString(withoutSpaces)
			if match != "" {
				// Remove the "<" and ">" symbols
				substring = match[1 : len(match)-1]
			} else {
				fmt.Println("No match found")
			}
		}
	}
	return substring
}

Мы добавили функцию копирования, поиска файла и непосредственной генерации случайного QR-кода (ранее мы выполнили привязку к номеру телефона с помощью CreateQR). Благодаря тому, что практически каждая строка настраивается под себя, можно быть уверенным в единственности сгенерированного сочетания символов.

Глава 2. Настройка параметров получателя и переадресации

После создания QR-кода, можно начинать им пользоваться — дать возможность нескольким своим знакомым перейти по ссылке и отправить сообщение. Внутри МТС Exolve можно проверить входящие сообщения с помощью API. 

Но прежде чем начинать переадресацию в другие приложения (CRM и базы данных), необходимо настроить параметры получателя — как программа будет получать доступ к полученным сообщениям и заниматься их отправкой. Для этого реализуем следующую часть кода:

package settings
import (
	"fmt"
	"os"
	"regexp"
	"strings"
)
var AccessToken string
var RefreshToken string
var LongRefreshToken string
var ClientID string
var ClientSecret string
var CodeAuth string
var RedirectUri string
var Subdomain string
var CrmPhoneField string
var ExolveAccessToken string
func InitSettings() {
	ClientID = FindSettings("ClientID")
	ClientSecret = FindSettings("ClientSecret")
	CodeAuth = FindSettings("CodeAuth")
	RedirectUri = FindSettings("RedirectUri")
	Subdomain = FindSettings("Subdomain")
	CrmPhoneField = FindSettings("CrmPhoneField")
	ExolveAccessToken = FindSettings("ExolveAccessToken")
}
func FindSettings(substr string) string {
	var substring string
	file, err := os.ReadFile("settings/settings")
	if err != nil {
		fmt.Println("Error reading")
	}
	list := strings.Split(string(file), ",")
	for k, _ := range list {
		withoutSpaces := strings.Join(strings.Fields(list[k]), "")
		if strings.Contains(withoutSpaces, substr) {
			pattern := `<([^>]+)>`
			re := regexp.MustCompile(pattern)
			match := re.FindString(withoutSpaces)
			if match != "" {
				// Remove the "<" and ">" symbols
				substring = match[1 : len(match)-1]
				//fmt.Println("Substring:", substring)
			} else {
				fmt.Println("No match found")
			}
		}
	}
	return substring
}

Эта часть кода отвечает за то, чтобы считать необходимые данные настройки (их мы пропишем далее) и убрать все дополнительные символы, в том числе «<» и «>». Это позволит буквально копировать и вставлять все нужные значения.

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

ClientID = <”ID клиента”>,
ClientSecret     = <”Секретный код клиента”>,
CodeAuth = <”Код авторизации клиента”>,
RedirectUri = <”Ссылка для переадрессации”>,
Subdomain = <”Субдомен”>,
CrmPhoneField = <”Поле для вставки телефона в CRM”>,
ExolveAccessToken = <”Код авторизации Exolve”>,

На данном этапе подразумевается, что у пользователя уже есть учетная запись внутри MTC Exolve и необходимой CRM-системы (в данном случае amoCRM), а также действующий сервер со всеми необходимыми настройками.

Глава 3. Работа с MTC Exolve

Благодаря тому, что Exolve активно работает над API, внутри системы можно найти огромное количество функций, в том числе чтение входящих сообщений в режиме реального времени. Это нам потребуется для передачи данных в amoCRM.

Для этого мы реализуем следующий код:

package exolve
import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"qr/amocrm"
	"qr/exolve/models"
	"qr/settings"
	"strconv"
	"time"
)
func SendSms(w http.ResponseWriter, r *http.Request) {
	var message models.IncomingMessage
	bs, err := io.ReadAll(r.Body)
	if err != nil {
		log.Println("error reading message")
	}
	err = json.Unmarshal(bs, &message)
	if err != nil {
		log.Println("error unmarshaling message: ", err)
	}
	w.Write([]byte(message.Sender)) //sender number phone
	fmt.Fprint(w, message)
	log.Println("message.Sender:", message.Sender)
	log.Println("message:", message)
	amocrm.CreateDealAndContact(message.Sender) //there we create deal and contact in AmoCrm when we get sms
}
func CreateBody(datestart string) []byte {
	dateGte, err := time.Parse(time.RFC3339, datestart)
	if err != nil {
		fmt.Println(err)
	}
	dateLte := time.Now().Format(time.RFC3339)

	command := fmt.Sprintf(`{
      "date_gte": "%s",
      "date_lte": "%s"
    }`, dateGte.Format(time.RFC3339), dateLte)
	return []byte(command)
}
func GetList() {
	uri := `https://api.exolve.ru/messaging/v1/GetList`
	dateStart := "2023-10-20T15:04:05.000Z" //YYYY-MM-DD
	//dateFinish := "2023-10-27T15:04:05.000Z" //YYYY-MM-DD
	//dateFinish we don't use, because instead dateFinish now we got current date
	body := CreateBody(dateStart)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		log.Println("")
	}
	req.Header.Add("Authorization", "Bearer "+settings.ExolveAccessToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	bs, _ := io.ReadAll(resp.Body)
	var response models.MessageResponse
	err = json.Unmarshal(bs, &response)
	if err != nil {
		fmt.Println("Error unmarshaling JSON:", err)
		return
	}
	/*for _, message := range response.Messages {
		fmt.Printf("Message ID: %s\n", message.MessageID)
		fmt.Printf("Application UUID: %s\n", message.ApplicationUUID)
		fmt.Printf("Date: %s\n", message.Date.Format(time.RFC3339))
		fmt.Printf("Number: %s\n", message.Number)
		fmt.Printf("Sender: %s\n", message.Sender)
		fmt.Printf("Receiver: %s\n", message.Receiver)
		fmt.Printf("Text: %s\n", message.Text)
		fmt.Printf("Direction: %d\n", message.Direction)
		fmt.Printf("Segments Count: %d\n", message.SegmentsCount)
		fmt.Printf("Billing Status: %d\n", message.BillingStatus)
		fmt.Printf("Delivery Status: %d\n", message.DeliveryStatus)
		fmt.Printf("Channel: %d\n", message.Channel)
		fmt.Printf("Status: %d\n", message.Status)
	}*/
}
func GetCount() {
	uri := `https://api.exolve.ru/messaging/v1/GetCount`
	dateStart := "2023-10-20T15:04:05.000Z" //YYYY-MM-DD
	//dateFinish := "2023-10-27T15:04:05.000Z" //YYYY-MM-DD
	body := CreateBody(dateStart)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		log.Println("")
	}
	req.Header.Add("Authorization", "Bearer "+settings.ExolveAccessToken)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	var response models.Count
	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
		log.Println("Error:", err)
		return
	}
	count, _ := strconv.Atoi(response.Count)
	fmt.Println("Count:", count)
}

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

В силу того, что эти функции используют уже готовый API, внутри него уже есть прописанные модели. Учтем их в процессе написания кода:

package models
import "time"
type Message struct {
	MessageID       string    `json:"message_id"`
	ApplicationUUID string    `json:"application_uuid"`
	Date            time.Time `json:"date"`
	Number          string    `json:"number"`
	Sender          string    `json:"sender"`
	Receiver        string    `json:"receiver"`
	Text            string    `json:"text"`
	Direction       int       `json:"direction"`
	SegmentsCount   int       `json:"segments_count"`
	BillingStatus   int       `json:"billing_status"`
	DeliveryStatus  int       `json:"delivery_status"`
	Channel         int       `json:"channel"`
	Status          int       `json:"status"`
}
type MessageResponse struct {
	Messages []Message `json:"messages"`
}
type Count struct {
	Count string `json:"count"`
}

Эта часть моделей используется для получения списка сообщений.

package models
import "time"
type IncomingMessage struct {
	EventID        string    `json:"event_id"`
	MessageID      string    `json:"message_id"`
	ApplicationID  string    `json:"application_id"`
	Date           time.Time `json:"date"`
	Sender         string    `json:"sender"`
	Receiver       string    `json:"receiver"`
	Text           string    `json:"text"`
	Direction      string    `json:"direction"`
	SegmentsCount  int       `json:"segments_count"`
	BillingStatus  string    `json:"billing_status"`
	DeliveryStatus string    `json:"delivery_status"`
	MessageChannel string    `json:"message_channel"`
	Status         string    `json:"status"`
}

А эта нужна для отправки сообщений в amoCRM.

Таким образом, работу с API Exolve можно здесь закончить — рекомендую проверять время от времени состояние баланса, чтобы сообщения корректно отправлялись. При возникновении затруднений в настройке API можно обратиться в службу технической поддержки.

Глава 4. Работа с amoCRM

Для передачи данных в базу amoCRM внутри системы есть API-интеграция с одноразовыми токенами. Именно поэтому каждый раз, когда сервер будет получать SMS-сообщение, он будет самостоятельно создавать уникальную сделку и прописывать в специальном поле номер телефона.

В amoCRM используют формат авторизации по протоколу OAuth 2.0, поэтому необходимо добавить соответствующие функции access_token и refresh_token. Если ранее не приходилось с ними сталкиваться, вот небольшая справка:

Access_token (токен доступа) — главный токен для взаимодействия (авторизации) с API amoCRM. Главная особенность — он действует всего 24 часа, после чего необходимо его автоматически обновлять. Для этого прописывается функция refresh_token.

Refresh_token (токен обновления) — второй токен, который формируется каждый раз, когда срабатывает функция access_token. В отличие от токена доступа, этот действует 15 дней и необходим для обновления access_token.

Если не использовать эту интеграцию с amoCRM более 15 дней, придется производить настройку заново, потому что истечет срок действия refresh_token. Но в плане реализации код не очень большой:

package amocrm
import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"qr/amocrm/models"
	"qr/settings"
)
func RefreshTokenAuth() {
	uri := `https://yourdomain.amocrm.ru/oauth2/access_token`
	form := url.Values{}
	form.Add("client_id", settings.ClientID)
	form.Add("grant_type", "refresh_token")
	form.Add("client_secret", settings.ClientSecret)
	form.Add("refresh_token", settings.RefreshToken)
	form.Add("redirect_uri", settings.RedirectUri)
	req, err := http.NewRequest("POST", uri, bytes.NewBufferString(form.Encode()))
	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
	}
	var response models.Token
	err = json.Unmarshal(body, &response)
	if err != nil {
		fmt.Println("Error unmarshaling JSON:", err)
		return
	}
	fmt.Println("Token Type:", response.TokenType)
	fmt.Println("Expires In:", response.ExpiresIn)
	fmt.Println("Access Token:", response.AccessToken)
	fmt.Println("Refresh Token:", response.RefreshToken)
	settings.AccessToken = response.AccessToken
	settings.RefreshToken = response.RefreshToken
	log.Println("LongRefreshToken:", settings.LongRefreshToken)
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Request failed with status: %s\n", resp.Status)
	} else {
		fmt.Println("Request successful!")
	}
}
func GetToken() {
	uri := fmt.Sprintf(`https://%s.amocrm.ru/oauth2/access_token`, settings.Subdomain)
	reqExample := fmt.Sprintf(`{
  "client_id": "%s",
  "client_secret": "%s",
  "grant_type": "authorization_code",
  "code": "%s",
  "redirect_uri": "%s"
}`, settings.ClientID, settings.ClientSecret, settings.CodeAuth, settings.RedirectUri)
	body := []byte(reqExample)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bodyRead, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
	}
	var response models.Token
	err = json.Unmarshal(bodyRead, &response)
	if err != nil {
		fmt.Println("Error unmarshaling JSON:", err)
		return
	}
	fmt.Println("Token Type:", response.TokenType)
	fmt.Println("Expires In:", response.ExpiresIn)
	fmt.Println("Access Token:", response.AccessToken)
	fmt.Println("Refresh Token:", response.RefreshToken)
	settings.AccessToken = response.AccessToken
	settings.RefreshToken = response.RefreshToken
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Request failed with status: %s\n", resp.Status)
	} else {
		fmt.Println("Request successful!")
	}
}

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

package amocrm
import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"qr/settings"
)
func GetDeals(w http.ResponseWriter, r *http.Request) {
	//tokenData, err := loadTokenDataFromFile()
	uri := fmt.Sprintf(`https://%s.amocrm.ru/api/v4/leads`, settings.Subdomain)
	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		fmt.Println("Error creating request:", err)
	}
	req.Header.Set("Content-Type", "application/json")
	//fmt.Println("tokenData.AccessToken:::", tokenData.AccessToken)
	req.Header.Set("Authorization", "Bearer "+settings.AccessToken)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error making the request:", err)
	}
	defer resp.Body.Close()
	response, _ := io.ReadAll(resp.Body)
	log.Println("response::", string(response))
	w.Write(response)
}
func CreateDealAndContact(sender string) {
	uri := fmt.Sprintf("https://%s.amocrm.ru/api/v4/leads/complex", settings.Subdomain)

	leadData := fmt.Sprintf(`[
   {
      "name": "Сбор номеров по QR",
      "_embedded":{
         "contacts":[
            {
               "first_name":"QR-Клиент",
              "custom_fields_values":[
                  {
                     "field_id":%s,
                     "values":[
                        {
                           "value":"+%s"}]}]}]}}]`, settings.CrmPhoneField, sender)
	body := []byte(leadData)
	/*jsonData, err := json.Marshal(leadData)
	if err != nil {
		fmt.Println("Error marshaling JSON:", err)
		return
	}
	fmt.Println(string(jsonData))*/

	//rs := bytes.NewReader(jsonData)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println("Error creating request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+settings.AccessToken)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error making the request:", err)
		return
	}
	defer resp.Body.Close()
	rdr, _ := io.ReadAll(resp.Body)
	fmt.Println("string(rdr): ", string(rdr))
	fmt.Println("Lead created successfully!")
}
func DealCreate() {
	uri := fmt.Sprintf("https://%s.amocrm.ru/api/v4/leads", settings.Subdomain)
	leadData := fmt.Sprintf(`[
	    {
	        "name": "Сделка для примера 1",
	        "price": 20000,
	    },
	    {
	        "name": "Сделка для примера 2",
	        "price": 10000,
	    }
	]`)
	body := []byte(leadData)
	/*jsonData, err := json.Marshal(leadData)
	if err != nil {
		fmt.Println("Error marshaling JSON:", err)
		return
	}
	fmt.Println(string(jsonData))*/
	//rs := bytes.NewReader(jsonData)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println("Error creating request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+settings.AccessToken)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error making the request:", err)
		return
	}
	defer resp.Body.Close()
	rdr, _ := io.ReadAll(resp.Body)
	fmt.Println("string(rdr): ", string(rdr))
	fmt.Println("Lead created successfully!")
}
func DealCreateHandler(w http.ResponseWriter, r *http.Request) {
	uri := fmt.Sprintf("https://%s.amocrm.ru/api/v4/leads", settings.Subdomain)
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}
	/*leadData := fmt.Sprintf(`[
	    {
	        "name": "Сделка для примера 1",
	        "price": 20000,
	    },
	    {
	        "name": "Сделка для примера 2",
	        "price": 10000,
	    }
	]`)*/
	//body := []byte(leadData)
	/*jsonData, err := json.Marshal(leadData)
	if err != nil {
		fmt.Println("Error marshaling JSON:", err)
		return
	}
	fmt.Println(string(jsonData))*/
	//rs := bytes.NewReader(jsonData)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println("Error creating request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+settings.AccessToken)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error making the request:", err)
		return
	}
	defer resp.Body.Close()
	rdr, _ := io.ReadAll(resp.Body)
	fmt.Println("string(rdr): ", string(rdr))
	sb, _ := io.ReadAll(resp.Body)
	w.Write(sb)
	fmt.Println("Lead created successfully!")
}
func CreateDealAndContactHandler(w http.ResponseWriter, r *http.Request) {
	uri := fmt.Sprintf("https://%s.amocrm.ru/api/v4/leads/complex", settings.Subdomain)
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}
	/*leadData := fmt.Sprintf(`[
	    {
	        "name": "Сделка для примера 1",
	        "price": 20000,
	    },
	    {
	        "name": "Сделка для примера 2",
	        "price": 10000,
	    }
	]`)*/
	//body := []byte(leadData)
	/*jsonData, err := json.Marshal(leadData)
	if err != nil {
		fmt.Println("Error marshaling JSON:", err)
		return
	}
	fmt.Println(string(jsonData))*/
	//rs := bytes.NewReader(jsonData)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println("Error creating request:", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+settings.AccessToken)
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println("Error making the request:", err)
		return
	}
	defer resp.Body.Close()
	rdr, _ := io.ReadAll(resp.Body)
	fmt.Println("string(rdr): ", string(rdr))
	sb, _ := io.ReadAll(resp.Body)
	w.Write(sb)
	fmt.Println("Lead created successfully!")
}

В этой части кода формируется сама сделка — с указанием соответствующей базы данных. Данные поступают непосредственно из Exolve с помощью соответствующего API. Есть приятная особенность этой части — базу можно использовать централизованно для нескольких методов получения — с помощью QR-кода, работы менеджеров или форм внутри сайтов. Пользователи, полученные с помощью QR будут прописываться как «QR-клиент», а номера будут расположены в поле «Рабочий телефон».

Дополнительно нам потребуется несколько функций — запись токена в файл и его загрузка в дальнейшем. Это позволит хранить данные на носителе. Реализуется с помощью кода:

package amocrm
import (
	"encoding/json"
	"fmt"
	"os"
	"qr/amocrm/models"
)
func writeTokenDataToFile(data models.TokenData) {
	jsonData, err := json.Marshal(data)
	if err != nil {
		fmt.Println("Error marshaling token data:", err)
		return
	}
	// Change the filename to the desired file path
	filename := "Название JSON-файла"
	err = os.WriteFile(filename, jsonData, 0644)
	if err != nil {
		fmt.Println("Error writing token data to file:", err)
	} else {
		fmt.Println("Token data written to file:", filename)
	}
}
func loadTokenDataFromFile() (models.TokenData, error) {
	// Change the filename to the path of your JSON file
	filename := "Название JSON-файла"
	file, err := os.ReadFile(filename)
	if err != nil {
		return models.TokenData{}, err
	}
	var tokenData models.TokenData
	err = json.Unmarshal(file, &tokenData)
	if err != nil {
		return models.TokenData{}, err
	}
	return tokenData, nil
}

Эта часть кода поможет в автоматизации процессов считывания и обновления токенов — к тому же они не потеряются, даже если что-то произойдет с сервером. Для тестирования работы системы мы оставляем несколько тестов:

package amocrm
import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"qr/amocrm/models"
	"qr/settings"
)
func GetTokenHandler(w http.ResponseWriter, r *http.Request) {
	uri := `https://onvizbitrix.amocrm.ru/oauth2/access_token`
	reqExample := fmt.Sprintf(`{
  "client_id": "%s",
  "client_secret": "%s",
  "grant_type": "authorization_code",
  "code": "%s",
  "redirect_uri": "%s"
}`, settings.ClientID, settings.ClientSecret, settings.CodeAuth, settings.RedirectUri)
	body := []byte(reqExample)
	req, err := http.NewRequest("POST", uri, bytes.NewReader(body))
	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	bodyRead, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
	}
	var response models.Token
	err = json.Unmarshal(bodyRead, &response)
	if err != nil {
		fmt.Println("Error unmarshaling JSON:", err)
		return
	}

	fmt.Println("Token Type:", response.TokenType)
	fmt.Println("Expires In:", response.ExpiresIn)
	fmt.Println("Access Token:", response.AccessToken)
	fmt.Println("Refresh Token:", response.RefreshToken)
	settings.AccessToken = response.AccessToken
	settings.RefreshToken = response.RefreshToken
	w.Write([]byte("AccessToken: " + settings.AccessToken))
	w.Write([]byte("RefreshToken: " + settings.RefreshToken))

	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Request failed with status: %s\n", resp.Status)
	} else {
		fmt.Println("Request successful!")
	}
}
func RefreshTokenAuthHandler(w http.ResponseWriter, r *http.Request) {
	uri := `https://onvizbitrix.amocrm.ru/oauth2/access_token`
	form := url.Values{}
	form.Add("client_id", settings.ClientID)
	form.Add("grant_type", "refresh_token")
	form.Add("client_secret", settings.ClientSecret)
	form.Add("refresh_token", settings.RefreshToken)
	form.Add("redirect_uri", settings.RedirectUri)
	req, err := http.NewRequest("POST", uri, bytes.NewBufferString(form.Encode()))
	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
	}
	var response models.Token
	err = json.Unmarshal(body, &response)
	if err != nil {
		fmt.Println("Error unmarshaling JSON:", err)
		return
	}
	fmt.Println("Token Type:", response.TokenType)
	fmt.Println("Expires In:", response.ExpiresIn)
	fmt.Println("Access Token:", response.AccessToken)
	fmt.Println("Refresh Token:", response.RefreshToken)
	settings.AccessToken = response.AccessToken
	settings.RefreshToken = response.RefreshToken
	log.Println("LongRefreshToken:", settings.LongRefreshToken)
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Request failed with status: %s\n", resp.Status)
	} else {
		fmt.Println("Request successful!")
	}
}

Эту часть кода необходимо использовать в том случае, если потребуется проверить работу формирования токенов для своего API в amoCRM. Она демонстрирует, правильно ли создаются значения.

Для корректной работы необходимо настроить модели API самого amoCRM. Нужно добавить несколько текстовых файлов со следующим содержимым:

package models
type TokenData struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

Этот файл используется для авторизации в системе.

package models
type CustomFieldValue struct {
	FieldID int `json:"field_id"`
	Values  []struct {
		Value string `json:"value"`
	} `json:"values"`
}
type Embedded struct {
	Tags []struct {
		ID int `json:"id"`
	} `json:"tags"`
}
type Lead struct {
	Name              string             `json:"name"`
	CreatedBy         int                `json:"created_by,omitempty"`
	Price             int                `json:"price"`
	CustomFieldValues []CustomFieldValue `json:"custom_fields_values,omitempty"`
	Embedded          Embedded           `json:"_embedded,omitempty"`
}

Этот файл нужен для корректной работы сделок.

package models
type Token struct {
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
}

Этот файл требуется для работы токенов.

Работу с amoCRM на этом этапе можно завершить. Мы описали алгоритм работы только с amoCRM. Если нужно подключить другую клиентскую систему, потребуется написать код практически с нуля из-за других систем авторизации. Самый простой вариант — автоматически перенаправлять данные в Excel-файл.

Глава 5. Объединение Exolve и amoCRM

После настройки всех кодов доступа можно переходить к настройке связей между MTC Exolve и CRM-системой. В целом, в этом процессе нет ничего сложного — внутри сервисов есть специальные API для упрощения работы:

package router
import (
	"net/http"
	"qr/amocrm"
	"qr/exolve"
	"qr/qr"
)
func InitRoutes() {
	http.HandleFunc("/generate", qr.HandleRequest)
	http.HandleFunc("/send_sms", exolve.SendSms)
	http.HandleFunc("/deal", amocrm.DealCreateHandler)
	http.HandleFunc("/deal_contact", amocrm.CreateDealAndContactHandler)
	http.HandleFunc("/get_deals", amocrm.GetDeals)
	//this routes for testing requests
	//http.HandleFunc("/redirect", amocrm.RedirectHandler)
	//http.HandleFunc("/token", amocrm.GetTokenHandler)
	//http.HandleFunc("/refresh_token", amocrm.RefreshTokenAuthHandler)
}

Мы добавили несколько дополнительных строк для тестирования программы — их можно удалить при необходимости. Но самое главное на этом этапе — мы сделали некоторый «транш», который вмещает в себя все необходимые API-функции.

Глава 6. Собираем все в одном месте

После настройки всех систем собираем итоговый main-файл. Код будет выглядеть следующим образом:

package main
import (
	"log"
	"net/http"
	"qr/amocrm"
	"qr/qr"
	"qr/router"
	"qr/settings"
)
func main() {
	qr.CreateQR()
	settings.InitSettings() //in this package we customize settings, example ClientID, ClientSecret and other...
	router.InitRoutes()     //there we have more additional methods
	amocrm.GetToken()

	if err := http.ListenAndServe(":9090", nil); err != nil {
		log.Println(err.Error())
		return
	}
}

После запуска на сервере программа начнёт работу в фоновом режиме. Останется только создать рекламное предложение со сгенерированным QR-кодом и разместить его. После постепенного наполнения базы контактов, можете ввести систему отправки бонусов и организовать изначальную рассылку.

Специально для тех, кто чувствует затруднения при работе с сырым кодом, добавили ссылку на GitHub с готовым архивом (потребуется поменять данные сервера, Exolve и amoCRM на собственные, ранее выделены). После этого будет два варианта запуска:

  1. Самый простой: распаковываем архив, заходим в корень проекта, пишем go run main.go.

  2. Фоновый запуск сервиса: заходим в корень проекта, пишем go build ./ ,получаем файл QR, его запускаем.

Подписывайтесь на наш хаб, чтобы не пропустить следующие статьи о применении API MTC Exolve.

Автор: Анастасия Кузнецова

Комментарии (1)


  1. fireapple
    20.09.2024 08:06

    Может, только многовато кода под такую задачку. Но в целом, зато побольше гибкости, если действительно что-то изменить приспичит