add fields with files need work

This commit is contained in:
Pavel 2024-05-10 18:34:34 +03:00
parent 75970295b3
commit 74dfb8d4be
8 changed files with 366 additions and 122 deletions

2
go.mod

@ -13,7 +13,7 @@ require (
github.com/twmb/franz-go v1.16.1
go.uber.org/zap v1.27.0
google.golang.org/protobuf v1.33.0
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240507175756-10399fe4c21f
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240510090920-72cb7d7da6e9
penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af
)

4
go.sum

@ -169,7 +169,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240223054633-6cb3d5ce45b6 h1:oV+/HNX+JPoQ3/GUx08hio7d45WpY0AMGrFs7j70QlA=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240223054633-6cb3d5ce45b6/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240507175756-10399fe4c21f h1:xUo4CsauxNgFhiTfv+5BKfF4Ekk6SHeR+ohwrBuJIrU=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240507175756-10399fe4c21f/go.mod h1:oRyhT55ctjqp/7ZxIzkR7OsQ7T/NLibsfrbb7Ytns64=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240510090920-72cb7d7da6e9 h1:rEOS5bsCduPSYv5QNvU8YvXroTOeqeSNP/833ZvCUAA=
penahub.gitlab.yandexcloud.net/backend/quiz/common.git v0.0.0-20240510090920-72cb7d7da6e9/go.mod h1:oRyhT55ctjqp/7ZxIzkR7OsQ7T/NLibsfrbb7Ytns64=
penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af h1:jQ7HaXSutDX5iepU7VRImxhikK7lV/lBKkiloOZ4Emo=
penahub.gitlab.yandexcloud.net/backend/quiz/core.git v0.0.0-20240219174804-d78fd38511af/go.mod h1:5S5YwjSXWmnEKjBjG6MtyGtFmljjukDRS8CwHk/CF/I=

@ -72,7 +72,6 @@ func Run(ctx context.Context, config initialize.Config, logger *zap.Logger) erro
amoClient := amoClient.NewAmoClient(amoClient.AmoDeps{
BaseApiURL: config.ApiURL,
UserInfoURL: config.UserInfoURL,
Logger: logger,
RedirectionURL: config.ReturnURL,
IntegrationID: config.IntegrationID,

@ -26,8 +26,6 @@ type Config struct {
IntegrationID string `env:"INTEGRATION_ID" envDefault:"2dbd6329-9be6-41f2-aa5f-964b9e723e49"`
// секрет интеграции
IntegrationSecret string `env:"INTEGRATION_SECRET" envDefault:"tNK3LwL4ovP0OBK4jKDHJ3646PqRJDOKQYgY6P2t6DCuV8LEzDzszTDY0Fhwmzc8"`
// uri о которому получать информацию о пользователе https://www.amocrm.ru/developers/content/crm_platform/account-info
UserInfoURL string `env:"USER_INFO_URL" envDefault:"https://penadigitaltech.amocrm.ru/api/v4/account"`
}
func LoadConfig() (*Config, error) {

@ -19,14 +19,24 @@ type DealReq struct {
}
type FieldsValues struct {
FieldID int `json:"field_id"`
Values []Values `json:"values"`
FieldID int `json:"field_id"`
Values []interface{} `json:"values"`
}
type Values struct {
Value string `json:"value"` // пока так пока не понятно
}
type ValuesFile struct {
Value ValueFile `json:"value"`
}
type ValueFile struct {
FileUUID string `json:"file_uuid"`
VersionUUID string `json:"version_uuid"`
FileName string `json:"file_name"`
FileSize int64 `json:"file_size"`
}
type Embedd struct {
Tags []Tag `json:"tags"` // Данные тегов, добавляемых к сделке
Contact []Contact `json:"contacts"` // Данные контактов, которые будет прикреплены к сделке
@ -110,3 +120,53 @@ type EmbeddedCreateCustomers struct {
RequestID string `json:"request_id"`
}
}
type CreateSession struct {
FileName string `json:"file_name"` // обязательное поле
FileSize int64 `json:"file_size"` // обязательное поле
FileUUID string `json:"file_uuid"` // UUID файла, для которого загружается новая версия файла. Если UUID не задан, то будет создан новый файл.
ContentType string `json:"content_type"` // MIME-тип файла
WithPreview bool `json:"with_preview"` // При установке данного флага для файла будет сгенерировано превью
}
// представляет данные о созданной сессии загрузки файла
type UploadSession struct {
SessionID int `json:"session_id"`
UploadURL string `json:"upload_url"`
MaxFileSize int64 `json:"max_file_size"`
MaxPartSize int64 `json:"max_part_size"`
}
// представляет информацию о загруженном файле
type UploadedFile struct {
UUID string `json:"uuid"`
Type string `json:"type"`
IsTrashed bool `json:"is_trashed"`
Name string `json:"name"`
SanitizedName string `json:"sanitized_name"`
Size int64 `json:"size"`
SourceID int `json:"source_id"`
VersionUUID string `json:"version_uuid"`
HasMultipleVersions bool `json:"has_multiple_versions"`
CreatedAt int64 `json:"created_at"`
CreatedBy struct {
ID int `json:"id"`
Type string `json:"type"`
} `json:"created_by"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at"`
DeletedBy interface{} `json:"deleted_by"`
Metadata Metadata `json:"metadata"`
Previews []PreviewFile `json:"previews"`
}
type Metadata struct {
Extension string `json:"extension"`
MIMEType string `json:"mime_type"`
}
type PreviewFile struct {
DownloadLink string `json:"download_link"`
Width int `json:"width"`
Height int `json:"height"`
}

@ -2,11 +2,9 @@ package tools
import (
"amocrm/internal/models"
"encoding/json"
"fmt"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"strings"
"time"
"unicode/utf8"
)
@ -69,105 +67,6 @@ func ToField(amoField []models.CustomField, entity model.EntityType) []model.Fie
return fields
}
func ConstructField(allAnswers []model.ResultAnswer, result model.AmoUsersTrueResults) ([]models.FieldsValues, []models.Contact, []models.Company, []models.Customer, error) {
dateCreating := time.Now().Unix()
entityFieldsMap := make(map[model.EntityType]map[int][]models.Values)
entityFieldsMap[model.LeadsType] = make(map[int][]models.Values)
entityFieldsMap[model.CompaniesType] = make(map[int][]models.Values)
entityFieldsMap[model.CustomersType] = make(map[int][]models.Values)
entityRules := make(map[model.EntityType][]model.FieldRule)
entityRules[model.LeadsType] = result.FieldsRule.Lead
entityRules[model.CompaniesType] = result.FieldsRule.Company
entityRules[model.CustomersType] = result.FieldsRule.Customer
for entityType, ruleList := range entityRules {
for _, rule := range ruleList {
for _, data := range allAnswers {
if fieldID, ok := rule.Questionid[int(data.QuestionID)]; ok {
values := entityFieldsMap[entityType][fieldID]
values = append(values, models.Values{Value: emojiUnicode(data.Content)})
entityFieldsMap[entityType][fieldID] = values
}
}
}
}
var leadFields []models.FieldsValues
var contactFields []models.FieldsValues
var companyFields []models.FieldsValues
var customerFields []models.FieldsValues
for entityType, fieldMap := range entityFieldsMap {
for fieldID, values := range fieldMap {
field := models.FieldsValues{
FieldID: fieldID,
Values: values,
}
switch entityType {
case model.LeadsType:
leadFields = append(leadFields, field)
case model.CompaniesType:
companyFields = append(companyFields, field)
case model.CustomersType:
customerFields = append(customerFields, field)
}
}
}
var resultInfo model.ResultContent
err := json.Unmarshal([]byte(result.Content), &resultInfo)
if err != nil {
return nil, nil, nil, nil, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactRuleMap := result.FieldsRule.Contact.ContactRuleMap
contactFields = addContactField(contactFields, resultInfo.Name, model.TypeContactName, contactRuleMap)
contactFields = addContactField(contactFields, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
contactFields = addContactField(contactFields, resultInfo.Text, model.TypeContactText, contactRuleMap)
contactFields = addContactField(contactFields, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
contactFields = addContactField(contactFields, resultInfo.Address, model.TypeContactAddress, contactRuleMap)
return leadFields, []models.Contact{
{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
},
}, []models.Company{
{
Name: fmt.Sprintf("Компания %d", result.AnswerID),
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: companyFields,
},
}, []models.Customer{
{
// в амо имя покупателя не может быть пустым, надо как то с этим жить
Name: name,
ResponsibleUserID: result.PerformerID,
//StatusID: ,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFields: customerFields,
RequestID: fmt.Sprint(result.AnswerID),
},
}, nil
}
func isEmoji(r rune) bool {
// https://symbl.cc/ru/unicode/blocks/emoticons/
@ -194,7 +93,7 @@ func isEmoji(r rune) bool {
(r >= 0xE0100 && r <= 0xE01EF) // Дополнение к селекторам вариантов начертания
}
func emojiUnicode(text string) string {
func EmojiUnicode(text string) string {
var result strings.Builder
for len(text) > 0 {
r, size := utf8.DecodeRuneInString(text)
@ -213,15 +112,14 @@ func emojiUnicode(text string) string {
return result.String()
}
func addContactField(contactFields []models.FieldsValues, fieldValue string, fieldType model.ContactQuizConfig, fieldMap map[string]int) []models.FieldsValues {
func AddContactFields(contactFields []models.FieldsValues, fieldValue string, fieldType model.ContactQuizConfig, fieldMap map[string]int) []models.FieldsValues {
if fieldValue != "" {
values := make([]interface{}, 0)
values = append(values, models.Values{Value: fieldValue})
contactFields = append(contactFields, models.FieldsValues{
FieldID: fieldMap[string(fieldType)],
Values: []models.Values{
{
Value: fieldValue,
},
},
Values: values,
})
}
return contactFields

@ -6,9 +6,11 @@ import (
"amocrm/internal/tools"
"amocrm/pkg/amoClient"
"context"
"encoding/json"
"fmt"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/dal"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/amo"
"strconv"
"strings"
@ -87,9 +89,9 @@ func (wc *PostDeals) startFetching(ctx context.Context) {
RequestID: strconv.Itoa(int(result.AnswerID)),
}
leadFields, contactData, companyData, customerToCreate, err := tools.ConstructField(allAnswers, result)
leadFields, contactData, companyData, customerToCreate, err := wc.constructField(ctx, allAnswers, result)
if err != nil {
wc.logger.Error("error serialization resultContent to model ResultContent", zap.Error(err))
wc.logger.Error("error construct fields", zap.Error(err))
return
}
@ -176,6 +178,124 @@ func (wc *PostDeals) saveDealToDB(ctx context.Context, resp []models.DealResp, a
return nil
}
func (wc *PostDeals) constructField(ctx context.Context, allAnswers []model.ResultAnswer, result model.AmoUsersTrueResults) ([]models.FieldsValues, []models.Contact, []models.Company, []models.Customer, error) {
dateCreating := time.Now().Unix()
entityFieldsMap := make(map[model.EntityType]map[int][]interface{})
entityFieldsMap[model.LeadsType] = make(map[int][]interface{})
entityFieldsMap[model.CompaniesType] = make(map[int][]interface{})
entityFieldsMap[model.CustomersType] = make(map[int][]interface{})
entityRules := make(map[model.EntityType][]model.FieldRule)
entityRules[model.LeadsType] = result.FieldsRule.Lead
entityRules[model.CompaniesType] = result.FieldsRule.Company
entityRules[model.CustomersType] = result.FieldsRule.Customer
for entityType, ruleList := range entityRules {
for _, rule := range ruleList {
for _, data := range allAnswers {
if fieldID, ok := rule.Questionid[int(data.QuestionID)]; ok {
fieldData, err := wc.amoRepo.AmoRepo.GetFieldByID(ctx, int32(fieldID))
if err != nil {
return nil, nil, nil, nil, err
}
if fieldData.Type == model.TypeAmoText {
values := entityFieldsMap[entityType][fieldID]
values = append(values, models.Values{Value: tools.EmojiUnicode(data.Content)})
entityFieldsMap[entityType][fieldID] = values
continue
}
if fieldData.Type == model.TypeFile && data.Content != "" {
value, err := wc.amoClient.UploadFileToAmo(data.Content, result.AccessToken)
if err != nil {
return nil, nil, nil, nil, err
}
values := entityFieldsMap[entityType][fieldID]
values = append(values, value)
entityFieldsMap[entityType][fieldID] = values
continue
}
}
}
}
}
var leadFields []models.FieldsValues
var contactFields []models.FieldsValues
var companyFields []models.FieldsValues
var customerFields []models.FieldsValues
for entityType, fieldMap := range entityFieldsMap {
for fieldID, values := range fieldMap {
field := models.FieldsValues{
FieldID: fieldID,
Values: values,
}
switch entityType {
case model.LeadsType:
leadFields = append(leadFields, field)
case model.CompaniesType:
companyFields = append(companyFields, field)
case model.CustomersType:
customerFields = append(customerFields, field)
}
}
}
var resultInfo model.ResultContent
err := json.Unmarshal([]byte(result.Content), &resultInfo)
if err != nil {
return nil, nil, nil, nil, err
}
name := resultInfo.Name
if name == "" {
name = fmt.Sprintf("empty name, quiz %d, triggered by answer - %d", result.QuizID, result.AnswerID)
}
contactRuleMap := result.FieldsRule.Contact.ContactRuleMap
contactFields = tools.AddContactFields(contactFields, resultInfo.Name, model.TypeContactName, contactRuleMap)
contactFields = tools.AddContactFields(contactFields, resultInfo.Phone, model.TypeContactPhone, contactRuleMap)
contactFields = tools.AddContactFields(contactFields, resultInfo.Text, model.TypeContactText, contactRuleMap)
contactFields = tools.AddContactFields(contactFields, resultInfo.Email, model.TypeContactEmail, contactRuleMap)
contactFields = tools.AddContactFields(contactFields, resultInfo.Address, model.TypeContactAddress, contactRuleMap)
return leadFields, []models.Contact{
{
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: contactFields,
},
}, []models.Company{
{
Name: fmt.Sprintf("Компания %d", result.AnswerID),
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFieldsValues: companyFields,
},
}, []models.Customer{
{
// в амо имя покупателя не может быть пустым, надо как то с этим жить
Name: name,
ResponsibleUserID: result.PerformerID,
CreatedBy: 0,
UpdatedBy: 0,
CreatedAt: dateCreating,
CustomFields: customerFields,
RequestID: fmt.Sprint(result.AnswerID),
},
}, nil
}
func (wc *PostDeals) Stop(_ context.Context) error {
return nil
}

@ -3,9 +3,13 @@ package amoClient
import (
"amocrm/internal/models"
"amocrm/internal/workers/limiter"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"time"
@ -15,7 +19,6 @@ import (
type Amo struct {
baseApiURL string
userInfoURL string
fiberClient *fiber.Client
logger *zap.Logger
redirectionURL string
@ -26,7 +29,6 @@ type Amo struct {
type AmoDeps struct {
BaseApiURL string
UserInfoURL string
FiberClient *fiber.Client
Logger *zap.Logger
RedirectionURL string
@ -41,7 +43,6 @@ func NewAmoClient(deps AmoDeps) *Amo {
}
return &Amo{
baseApiURL: deps.BaseApiURL,
userInfoURL: deps.UserInfoURL,
fiberClient: deps.FiberClient,
logger: deps.Logger,
redirectionURL: deps.RedirectionURL,
@ -277,7 +278,8 @@ func (a *Amo) GetListTags(req models.GetListTagsReq, accessToken string) (*model
func (a *Amo) GetUserInfo(accessToken string) (*models.AmocrmUserInformation, error) {
for {
if a.rateLimiter.Check() {
agent := a.fiberClient.Get(a.userInfoURL)
url := fmt.Sprintf("%s/api/v4/account", a.baseApiURL)
agent := a.fiberClient.Get(url)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
@ -539,3 +541,170 @@ func (a *Amo) CreatingCustomer(req []models.Customer, accessToken string) (*mode
time.Sleep(a.rateLimiter.Interval)
}
}
func (a *Amo) downloadFile(urlFile string) (*os.File, error) {
var err error
agent := a.fiberClient.Get(urlFile)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request for getting file by url", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from getting file by url: %s", string(resBody))
a.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
tmpFile, err := os.CreateTemp("", "downloaded_file_*")
if err != nil {
return nil, err
}
defer tmpFile.Close()
_, err = io.Copy(tmpFile, bytes.NewReader(resBody))
if err != nil {
return nil, err
}
return tmpFile, nil
}
func (a *Amo) UploadFileToAmo(urlFile string, accessToken string) (*models.ValuesFile, error) {
fmt.Println(urlFile)
localFile, err := a.downloadFile(urlFile)
if err != nil {
return nil, err
}
defer os.Remove(localFile.Name())
fileInfo, err := os.Stat(localFile.Name())
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
createSessionData := &models.CreateSession{
FileName: urlFile,
FileSize: fileSize,
}
uri := fmt.Sprintf("%s/v1.0/sessions", "https://drive-b.amocrm.ru")
bodyBytes, err := json.Marshal(createSessionData)
if err != nil {
a.logger.Error("error marshal create session data:", zap.Error(err))
return nil, err
}
agent := a.fiberClient.Post(uri)
agent.Set("Content-Type", "application/json").Body(bodyBytes)
agent.Set("Authorization", "Bearer "+accessToken)
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request to create session for upload file in amo", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != fiber.StatusOK {
errorMessage := fmt.Sprintf("received an incorrect response from creating upload file session: %s", string(resBody))
a.logger.Error(errorMessage, zap.Int("status", statusCode))
return nil, fmt.Errorf(errorMessage)
}
var resp models.UploadSession
err = json.Unmarshal(resBody, &resp)
if err != nil {
a.logger.Error("error unmarshal response body in creating upload file session:", zap.Error(err))
return nil, err
}
response, err := a.createPart(resp, localFile)
return &models.ValuesFile{
Value: models.ValueFile{
FileUUID: response.UUID,
VersionUUID: response.VersionUUID,
FileName: response.Name,
FileSize: response.Size,
},
}, nil
}
func (a *Amo) createPart(uploadData models.UploadSession, file *os.File) (*models.UploadedFile, error) {
fileInfo, err := file.Stat()
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
var uploadedFile models.UploadedFile
var remainingSize = fileSize
var start int64 = 0
for remainingSize > 0 {
end := start + uploadData.MaxPartSize
if end > fileSize {
end = fileSize
}
partSize := end - start
partFile, err := os.OpenFile(file.Name(), os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
defer partFile.Close()
_, err = partFile.Seek(start, io.SeekStart)
if err != nil {
return nil, err
}
buffer := make([]byte, partSize)
_, err = partFile.Read(buffer)
if err != nil {
return nil, err
}
agent := a.fiberClient.Post(uploadData.UploadURL).Body(buffer)
if err != nil {
return nil, err
}
statusCode, resBody, errs := agent.Bytes()
if len(errs) > 0 {
for _, err = range errs {
a.logger.Error("error sending request to upload part file to amo", zap.Error(err))
}
return nil, fmt.Errorf("request failed: %v", errs[0])
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("failed to upload part file to amo, status: %d", statusCode)
}
start = end
remainingSize -= partSize
var newUploadData models.UploadSession
if err := json.Unmarshal(resBody, &newUploadData); err == nil {
uploadData = newUploadData
} else {
if err := json.Unmarshal(resBody, &uploadedFile); err != nil {
return nil, err
}
break
}
}
return &uploadedFile, nil
}