0xc3 1 month ago
parent
commit
00cba1fc67
4 changed files with 136 additions and 360 deletions
  1. 29 15
      migrations/00001_clients_table.sql
  2. 1 84
      src/cmd/immigration/clients/main.go
  3. 96 259
      src/server/db/repo/client.go
  4. 10 2
      src/server/server.go

+ 29 - 15
migrations/00001_clients_table.sql

@@ -1,31 +1,45 @@
 -- +goose Up
+
 CREATE TABLE clients (
   id TEXT PRIMARY KEY,
-  id2 TEXT,
-  mark TEXT,
-  contractor INTEGER,
+  old_id TEXT,
+  contractor BOOLEAN,
   full_name TEXT,
-  type TEXT,
+  type TEXT NOT NULL,
   email TEXT,
   legal_address TEXT,
   physical_address TEXT,
-  registration_date TEXT,
-  ad_channel TEXT,
-  reg_data_1 TEXT,
-  reg_data_2 TEXT,
   note TEXT,
-  request_count INTEGER,
-  birthday TEXT,
-  income NUMERIC -- NUMERIC (или REAL) для числового значения
+  income_amount DECIMAL(19, 4),
+  income_currency CHAR(3)
+);
+
+CREATE TABLE client_tags (
+    client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
+    tag TEXT NOT NULL,
+    PRIMARY KEY (client_id, tag)
 );
 
 CREATE TABLE client_phones (
-  client_id INTEGER,
-  phone TEXT,
-  FOREIGN KEY (client_id) REFERENCES clients (id)
+    client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
+    phone TEXT NOT NULL,
+    PRIMARY KEY (client_id, phone)
 );
 
+CREATE TABLE client_metadata (
+    client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
+    key TEXT NOT NULL,
+    value TEXT NOT NULL,
+    PRIMARY KEY (client_id, key)
+);
+
+-- Индексы для производительности
+CREATE INDEX idx_clients_type ON clients(type);
+CREATE INDEX idx_clients_income ON clients(income_amount);
+CREATE INDEX idx_clients_currency ON clients(income_currency);
+
 -- +goose Down
+DROP TABLE client_metadata;
 DROP TABLE client_phones;
-
+DROP TABLE client_tags;
 DROP TABLE clients;

+ 1 - 84
src/cmd/immigration/clients/main.go

@@ -3,11 +3,8 @@ package main
 import (
 	"fmt"
 	"os"
-	"saura/src/common"
 	clientApi "saura/src/server/client"
 	"saura/src/server/db/repo"
-	"strconv"
-	"strings"
 
 	"github.com/xuri/excelize/v2"
 
@@ -25,7 +22,6 @@ func ReadClientsFromFile(file_path string) ([]repo.ClientInfo, error) {
 		return clients, err
 	}
 
-	// Get all the rows in the Sheet1.
 	rows, err := file.GetRows("Клиенты")
 	if err != nil {
 		fmt.Println(err)
@@ -33,89 +29,10 @@ func ReadClientsFromFile(file_path string) ([]repo.ClientInfo, error) {
 	}
 
 	// Пропускаем заголовок и обрабатываем каждую строку
-	for i, row := range rows {
+	for i, _ := range rows {
 		if i == 0 {
 			continue
 		}
-		client := repo.ClientInfo{}
-		client.Id2 = row[0]
-		if len(row) > 1 {
-			client.Mark = row[1]
-		}
-		if len(row) > 2 {
-			if row[2] == "Да" {
-				client.Contractor = true
-			} else {
-				client.Contractor = false
-			}
-		}
-		if len(row) > 3 {
-			client.FullName = row[3]
-		}
-		if len(row) > 4 {
-			client.Type = row[4]
-		}
-		if len(row) > 5 {
-			if row[5] != "" {
-				phones := strings.Split(row[5], ",")
-				client.Phones = phones
-				// fmt.Println("PHones:", client.Phones)
-			}
-		}
-		if len(row) > 6 {
-			client.Email = row[6]
-		}
-		if len(row) > 7 {
-			client.LegalAddress = row[7]
-		}
-		if len(row) > 8 {
-			client.PhysicalAddress = row[8]
-		}
-		if len(row) > 9 {
-			if row[9] != "" {
-				client.RegistrationDate = row[9]
-			}
-		}
-		if len(row) > 10 {
-			client.AdChannel = row[10]
-		}
-		if len(row) > 11 {
-			client.RegData1 = row[11]
-		}
-		if len(row) > 12 {
-			client.RegData2 = row[12]
-		}
-		if len(row) > 13 {
-			client.Note = row[13]
-		}
-		if len(row) > 14 {
-			if row[14] != "" {
-				value, err := strconv.Atoi(row[14])
-				if err != nil {
-					fmt.Println("Error string to int:", err)
-					return clients, err
-				}
-				client.RequestCount = value
-			}
-		}
-		if len(row) > 15 {
-			client.Birthday = row[15]
-		}
-		if len(row) > 16 {
-			str := row[16]
-			if str != "" {
-				if strings.Contains(str, ".") {
-					res := strings.ReplaceAll(row[16], ".", "")
-					parse, _ := strconv.ParseInt(res, 10, 64)
-					client.Income = common.Money(parse)
-				} else {
-					parse, _ := strconv.ParseInt(str, 10, 64)
-					client.Income = common.Money(parse * 100)
-				}
-			}
-		}
-		// fmt.Println("Client:", client)
-		clients = append(clients, client)
 	}
 
 	return clients, err

+ 96 - 259
src/server/db/repo/client.go

@@ -2,8 +2,9 @@ package repo
 
 import (
 	"database/sql"
-	"fmt"
-	"saura/src/common"
+	"math/rand"
+
+	"github.com/google/uuid"
 )
 
 const (
@@ -15,277 +16,113 @@ const (
 	ClientTypeCustomer     string = "Покупатель"
 )
 
-type ClientInfo struct {
-	Id               string       `db:"id" json:"id" form:"id"`
-	Id2              string       `db:"id2" json:"id2" form:"id2"`
-	Mark             string       `db:"mark" json:"mark" form:"mark"`
-	Contractor       bool         `db:"contractor" json:"contractor" form:"contractor"`
-	FullName         string       `db:"full_name" json:"full_name" form:"full_name"`
-	Type             string       `db:"type" json:"type" form:"type"`
-	Phones           []string     `json:"phones" form:"phones"`
-	Email            string       `db:"email" json:"email" form:"email"`
-	LegalAddress     string       `db:"legal_address" json:"legal_address" form:"legal_address"`
-	PhysicalAddress  string       `db:"physical_address" json:"physical_address" form:"physical_address"`
-	RegistrationDate string       `db:"registration_date" json:"registration_date" form:"registration_date"`
-	AdChannel        string       `db:"ad_channel" json:"ad_channel" form:"ad_channel"`
-	RegData1         string       `db:"reg_data_1" json:"reg_data_1" form:"reg_data_1"`
-	RegData2         string       `db:"reg_data_2" json:"reg_data_2" form:"reg_data_2"`
-	Note             string       `db:"note" json:"note" form:"note"`
-	RequestCount     int          `db:"request_count" json:"request_count" form:"request_count"`
-	Birthday         string       `db:"birthday" json:"birthday" form:"birthday"`
-	Income           common.Money `db:"income" json:"income" form:"income"`
-}
-
-type ClientRepo struct {
-	db    *sql.DB
-	dbUrl string
-}
-
-func NewClientRepo(db *sql.DB, dbUrl string) *ClientRepo {
-	return &ClientRepo{db: db, dbUrl: dbUrl}
+type Money struct {
+	Amount   int64  `json:"amount"`
+	Currency string `json:"currency"`
 }
 
-func (r *ClientRepo) InsertClient(client ClientInfo) error {
-	var err error
-	var exists bool
-
-	err = r.db.QueryRow("SELECT EXISTS(SELECT 1 FROM clients WHERE id = ? OR id2 = ?)", client.Id, client.Id2).Scan(&exists)
-	if err != nil {
-		return fmt.Errorf("failed to check if client exists: %w", err)
-	}
-	if exists {
-		return fmt.Errorf("client with id %s already exists", client.Id)
-	}
-
-	_, err = r.db.Exec(
-		`
-    INSERT INTO clients (
-      id, id2, mark, contractor, full_name, type, email,
-      legal_address, physical_address, registration_date, ad_channel,
-      reg_data_1, reg_data_2, note, request_count, birthday, income
-    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
-    `,
-		client.Id, client.Id2, client.Mark, client.Contractor, client.FullName,
-		client.Type, client.Email, client.LegalAddress,
-		client.PhysicalAddress, client.RegistrationDate, client.AdChannel,
-		client.RegData1, client.RegData2, client.Note, client.RequestCount,
-		client.Birthday, client.Income,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to insert client: %w", err)
-	}
-
-	return nil
+type ClientInfo struct {
+	ID              string `db:"id" json:"id"`
+	OldID           string `db:"old_id" json:"old_id"`
+	Contractor      bool   `db:"contractor" json:"contractor"`
+	FullName        string `db:"full_name" json:"full_name"`
+	Type            string `db:"type" json:"type"`
+	Email           string `db:"email" json:"email"`
+	LegalAddress    string `db:"legal_address" json:"legal_address"`
+	PhysicalAddress string `db:"physical_address" json:"physical_address"`
+	Note            string `db:"note" json:"note"`
+
+	Income Money `json:"income"`
+
+	Tags     []string          `json:"tags"`
+	Phones   []string          `json:"phones"`
+	Metadata map[string]string `json:"metadata"`
 }
 
-func (r *ClientRepo) FetchClientByID(id string) (*ClientInfo, error) {
-	var err error
-
-	row := r.db.QueryRow(`
-		SELECT
-    	id, id2, mark, contractor, full_name, type, email,
-      legal_address, physical_address, registration_date, ad_channel,
-      reg_data_1, reg_data_2, note, request_count, birthday, income
-    FROM clients
-    WHERE id = ?
-    `, id)
-
-	var client ClientInfo
-	err = row.Scan(
-		&client.Id, &client.Id2, &client.Mark, &client.Contractor, &client.FullName,
-		&client.Type, &client.Email, &client.LegalAddress,
-		&client.PhysicalAddress, &client.RegistrationDate, &client.AdChannel,
-		&client.RegData1, &client.RegData2, &client.Note, &client.RequestCount,
-		&client.Birthday, &client.Income,
-	)
-
-	if err == sql.ErrNoRows {
-		return nil, fmt.Errorf("client with id %s not found", id)
-	} else if err != nil {
-		return nil, fmt.Errorf("failed to scan row: %w", err)
-	}
-
-	phones, err := r.FetchClientPhones(client.Id)
-	if err != nil {
-		return nil, fmt.Errorf("error: %w", err)
-	}
-	client.Phones = phones
-
-	return &client, nil
+// ClientTag представляет запись из таблицы client_tags
+type ClientTag struct {
+	ClientID string `db:"client_id"`
+	Tag      string `db:"tag"`
 }
 
-func (r *ClientRepo) InsertClientPhones(client_id string, phones []string) error {
-	for _, phone := range phones {
-		_, err := r.db.Exec("INSERT INTO client_phones VALUES(?, ?)", client_id, phone)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
+// ClientPhone представляет запись из таблицы client_phones
+type ClientPhone struct {
+	ClientID string `db:"client_id"`
+	Phone    string `db:"phone"`
 }
 
-func (r *ClientRepo) FetchClientPhones(client_id string) ([]string, error) {
-	rows, err := r.db.Query("SELECT phone FROM client_phones WHERE client_id = ?", client_id)
-	if err != nil {
-		return nil, err
-	}
-	defer rows.Close()
-
-	var tags []string
-	for rows.Next() {
-		var tag string
-		rows.Scan(&tag)
-		tags = append(tags, tag)
-	}
-	return tags, nil
+// ClientMetadata представляет запись из таблицы client_metadata
+type ClientMetadata struct {
+	ClientID string `db:"client_id"`
+	Key      string `db:"key"`
+	Value    string `db:"value"`
 }
 
-func (r *ClientRepo) FetchClients(count int, offset int) ([]ClientInfo, error) {
-	rows, err := r.db.Query(`
-        SELECT
-            id, id2, mark, contractor, full_name, type, email,
-            legal_address, physical_address, registration_date, ad_channel,
-            reg_data_1, reg_data_2, note, request_count, birthday, income
-        FROM clients
-        LIMIT ? OFFSET ?
-    `, count, offset)
-	if err != nil {
-		return nil, fmt.Errorf("failed to execute query: %w", err)
-	}
-	defer rows.Close()
-
-	var clients []ClientInfo
-	for rows.Next() {
-		var client ClientInfo
-		err := rows.Scan(
-			&client.Id,
-			&client.Id2,
-			&client.Mark,
-			&client.Contractor,
-			&client.FullName,
-			&client.Type,
-			&client.Email,
-			&client.LegalAddress,
-			&client.PhysicalAddress,
-			&client.RegistrationDate, // time.Time или sql.NullTime
-			&client.AdChannel,
-			&client.RegData1,
-			&client.RegData2,
-			&client.Note,         // sql.NullString
-			&client.RequestCount, // int или sql.NullInt32
-			&client.Birthday,     // time.Time или sql.NullTime
-			&client.Income,       // float64 или sql.NullFloat64
-		)
-		if err != nil {
-			return nil, fmt.Errorf("failed to scan row: %w", err)
-		}
-
-		phones, err := r.FetchClientPhones(client.Id)
-		if err != nil {
-			return nil, fmt.Errorf("rows error: %w", err)
-		}
-		client.Phones = phones
-
-		clients = append(clients, client)
-	}
-
-	// Проверка ошибок после итерации
-	if err := rows.Err(); err != nil {
-		return nil, fmt.Errorf("rows error: %w", err)
-	}
-
-	return clients, nil
+type ClientRepo struct {
+	db    *sql.DB
+	dbUrl string
 }
 
-func (r *ClientRepo) FetchClientsCount() (int, error) {
-	var count int
-	query := "SELECT COUNT(*) FROM clients"
-	err := r.db.QueryRow(query).Scan(&count)
-	if err != nil {
-		return 0, fmt.Errorf("failed to fetch count: %w", err)
-	}
-	return count, nil
+func NewClientRepo(db *sql.DB, dbUrl string) *ClientRepo {
+	return &ClientRepo{db: db, dbUrl: dbUrl}
 }
 
-func (r *ClientRepo) DeleteClient(id string) error {
-	query := "DELETE FROM clients WHERE id = ?"
-	result, err := r.db.Exec(query, id)
-	if err != nil {
-		return fmt.Errorf("failed to delete client: %w", err)
-	}
-
-	rowsAffected, err := result.RowsAffected()
-	if err != nil {
-		return fmt.Errorf("failed to get rows affected: %w", err)
-	}
-
-	if rowsAffected == 0 {
-		return fmt.Errorf("client with id %s not found", id)
-	}
-
-	return nil
+func (r *ClientRepo) FetchClients(limit int, offset int) ([]ClientInfo, error) {
+	var clients = []ClientInfo{}
+	return clients[offset:limit], nil
 }
 
-func (r *ClientRepo) UpdateClient(id string, client ClientInfo) error {
-	// Проверим, существует ли клиент
-	var exists string
-	err := r.db.QueryRow("SELECT id FROM clients WHERE id = ?", id).Scan(&exists)
-	if err == sql.ErrNoRows {
-		return fmt.Errorf("client with id %s not found", id)
-	} else if err != nil {
-		return fmt.Errorf("failed to check client existence: %w", err)
-	}
-
-	query := `
-        UPDATE clients SET
-            id = ?,
-            id2 = ?,
-            mark = ?,
-            contractor = ?,
-            full_name = ?,
-            type = ?,
-            email = ?,
-            legal_address = ?,
-            physical_address = ?,
-            registration_date = ?,
-            ad_channel = ?,
-            reg_data_1 = ?,
-            reg_data_2 = ?,
-            note = ?,
-            request_count = ?,
-            birthday = ?,
-            income = ?
-        WHERE id = ?
-    `
-
-	contractorInt := 0
-	if client.Contractor {
-		contractorInt = 1
-	}
-
-	_, err = r.db.Exec(query,
-		id,
-		client.Id2,
-		client.Mark,
-		contractorInt,
-		client.FullName,
-		client.Type,
-		client.Email,
-		client.LegalAddress,
-		client.PhysicalAddress,
-		client.RegistrationDate,
-		client.AdChannel,
-		client.RegData1,
-		client.RegData2,
-		client.Note,
-		client.RequestCount,
-		client.Birthday,
-		client.Income,
-		id,
-	)
-	if err != nil {
-		return fmt.Errorf("failed to update client: %w", err)
-	}
-
-	return nil
+func (r *ClientRepo) FetchExampleClients(limit int, offset int) []ClientInfo {
+	rand.Seed(42)
+
+	clients := []ClientInfo{
+		{
+			ID:              uuid.New().String(),
+			FullName:        "Иванов Иван Иванович",
+			Type:            ClientTypePhysical,
+			Contractor:      false,
+			Email:           "ivanov@example.com",
+			PhysicalAddress: "г. Москва, ул. Тверская, д. 1, кв. 42",
+			Note:            "Постоянный клиент",
+			Income:          Money{19999, "RUB"},
+			Phones:          []string{"+79001234567", "+74951234567"},
+			Tags:            []string{"vip", "active"},
+			Metadata:        map[string]string{"source": "website", "referral": "google"},
+		},
+		{
+			ID:           uuid.New().String(),
+			FullName:     "ООО 'ТехноСервис'",
+			Type:         ClientTypeLegal,
+			Contractor:   true,
+			Email:        "office@technoservice.ru",
+			LegalAddress: "125009, г. Москва, ул. Тверская, д. 7",
+			Note:         "Основной контрагент",
+			Income:       Money{10090, "RUB"},
+			Phones:       []string{"+74959876543"},
+			Tags:         []string{"partner", "reliable"},
+			Metadata:     map[string]string{"industry": "it", "employees": "50+"},
+		},
+		{
+			ID:           uuid.New().String(),
+			FullName:     "ООО 'ГлобалСапплай'",
+			Type:         ClientTypeSupplier,
+			Contractor:   true,
+			Email:        "supply@globalsupply.ru",
+			LegalAddress: "190000, г. Санкт-Петербург, Невский пр., д. 28",
+			Note:         "Основной поставщик оборудования",
+			Income:       Money{15000, "EUR"},
+			Phones:       []string{"+74951112233", "+78123334455"},
+			Tags:         []string{"supplier", "international"},
+			Metadata:     map[string]string{"currency": "EUR", "delivery_time": "2 weeks"},
+		},
+	}
+
+	if offset >= len(clients) {
+		return []ClientInfo{}
+	}
+	if limit+offset > len(clients) {
+		limit = len(clients) - offset
+	}
+
+	return clients[offset : offset+limit]
 }

+ 10 - 2
src/server/server.go

@@ -56,8 +56,8 @@ func RegisterHandlers(db *db.DB) http.Handler {
 		}
 	})
 
-	e.GET("/", func(c echo.Context) error {
-		return c.String(http.StatusOK, "is live")
+	apiGroup.GET("/ping", func(c echo.Context) error {
+		return c.String(http.StatusOK, "")
 	})
 
 	apiGroup.GET("/v1/clients", func(c echo.Context) error {
@@ -71,5 +71,13 @@ func RegisterHandlers(db *db.DB) http.Handler {
 		return c.JSON(http.StatusOK, clients)
 	})
 
+	apiGroup.GET("/v1/example/clients", func(c echo.Context) error {
+		limit, _ := strconv.Atoi(c.QueryParam("limit"))
+		offset, _ := strconv.Atoi(c.QueryParam("offset"))
+
+		clients := db.GetClientRepo().FetchExampleClients(limit, offset)
+		return c.JSON(http.StatusOK, clients)
+	})
+
 	return e
 }