Entwickler, der außergewöhnliche CRM- und Laravel-Lösungen liefert

Als erfahrener Entwickler spezialisiere ich mich auf Laravel- und Vue.js-Entwicklung, die Implementierung von Vtiger CRM sowie auf vielfältige WordPress-Projekte. Meine Arbeit zeichnet sich durch kreative, dynamische und benutzerzentrierte Weblösungen aus, die individuell an die Bedürfnisse meiner Kunden angepasst werden.

In der heutigen Welt hilft die Automatisierung von Routineprozessen, Zeit zu sparen und Fehler zu vermeiden. In einem meiner Projekte hatte ich die Aufgabe, SQL-Skripte, die in einem bestimmten Ordner auftauchen, automatisch auszuführen und dann einen vollständigen Bericht über die Ausführung an die E-Mail des Administrators zu senden.

Automatisierte SQL-Ausführung mit Go: Entwicklungserfahrung und Einrichtung von Benachrichtigungen

In der heutigen Welt hilft die Automatisierung von Routineprozessen, Zeit zu sparen und Fehler zu vermeiden. In einem meiner Projekte hatte ich die Aufgabe, SQL-Skripte, die in einem bestimmten Ordner auftauchen, automatisch auszuführen und dann einen vollständigen Bericht über die Ausführung an die E-Mail des Administrators zu senden. Um diese Aufgabe zu lösen, beschloss ich, einen spezialisierten Microservice in der Sprache Go zu schreiben, der das angegebene Verzeichnis ständig überwacht, die gefundenen SQL-Dateien ausführt und die zuständigen Personen über die Ergebnisse der Arbeit benachrichtigt. Im Folgenden werde ich die Schritte der Implementierung, die Verwendung einer externen Konfigurationsdatei und die Besonderheiten des Ansatzes im Detail beschreiben.

Schritt 1: Projektinitialisierung und Anwendungskonfiguration

Der erste Schritt bestand darin, die Projektstruktur zu entwickeln und eine Konfigurationsdatei zu erstellen. Um flexibel arbeiten zu können, werden alle Einstellungen - Datenbankverbindungsparameter, Informationen für SMTP-Benachrichtigungen und Pfade zu Arbeitsordnern - in einer separaten Datei config.yml abgelegt. Das macht es einfach, die Parameter zu ändern, ohne den Quellcode zu verändern.

database:
  host: localhost
  port: 3306
  user: root
  password: secret
  name: mydb

smtp:
  server: smtp.example.com
  port: 587
  username: user@example.com
  password: smtp-password
  from: noreply@example.com
  to: admin@example.com

paths:
  input: ./sql/input
  output: ./sql/processed
  error: ./sql/errors

Schritt 2: Verbindung mit der Datenbank herstellen

Nachdem Sie die Einstellungen aus der Datei geladen haben, müssen Sie eine Verbindung zur Datenbank herstellen. Zu diesem Zweck habe ich ein Paket für die Arbeit mit MySQL verwendet. Es ist notwendig, eine Verbindung zur Datenbank herzustellen und die Korrektheit der Interaktion mit der Datenbank zu überprüfen.

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

Schritt 3: Überwachung von Verzeichnissen und Verarbeitung von Dateien

Die Hauptfunktion des Skripts besteht darin, ein bestimmtes Verzeichnis ständig zu überwachen. Alle paar Sekunden prüft die Anwendung, ob neue SQL-Dateien vorhanden sind, und wenn eine gefunden wird, liest sie den Inhalt der Datei und versucht, SQL-Abfragen in der Datenbank auszuführen.

Die Merkmale dieser Phase sind:

  • Überprüfung jeder Datei auf zusätzliche Zeichen (z. B. BOM) - dies ist wichtig für die korrekte Ausführung von Abfragen.
  • Protokollierung von Fehlern beim Lesen oder Ausführen von SQL-Abfragen zur Analyse von Abstürzen.
  • Trennung der Dateien in erfolgreich ausgeführte und solche, die einen Fehler verursacht haben, durch Verschieben in die entsprechenden Ordner (erledigt und fehlgeschlagen).
func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

Schritt 4: Implementierung der Logik für die Ausführung von SQL-Abfragen

Jede SQL-Datei wird vor der Ausführung geprüft. Danach wird eine SQL-Abfrage mit db.Exec ausgeführt. Wenn die Abfrage erfolgreich ausgeführt wurde, wird die Datei in den Ordner für archivierte Skripte verschoben; andernfalls wird sie in den Ordner für Fehler verschoben. Auf diese Weise wird die wiederholte Ausführung derselben Abfrage vermieden, und es kann festgestellt werden, welche Dateien wiederholt bearbeitet werden müssen.

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

Schritt 5: Versenden des Fortschrittsberichts per E-Mail

Am Ende der Skriptoperation ist es wichtig, einen Bericht an den Administrator zu senden. Jedes Mal, wenn ein Vorgang ausgeführt wurde (erfolgreich oder mit einem Fehler), wird eine entsprechende Benachrichtigung über SMTP gesendet. Durch die Verwendung einer sicheren TLS-Verbindung wird die Sicherheit der Datenübertragung gewährleistet. So erhalten Sie schnell eine Rückmeldung über den Skriptbetrieb und können möglichen Problemen im Systembetrieb vorbeugen.

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

Vor- und Nachteile des Ansatzes

Vorteile:

  • Die Automatisierung von Routineaufgaben kann die Bearbeitungszeit erheblich verkürzen und das Risiko von Fehlern verringern.
  • Die Flexibilität der Einstellungen über eine externe Konfigurationsdatei ermöglicht die einfache Anpassung des Skripts an unterschiedliche Umgebungen.
  • Der Versand von Berichten per E-Mail stellt sicher, dass die zuständigen Personen zeitnah informiert werden.

Nachteilig:

  • Die ständige Überprüfung des Disk-Ordners kann das System bei einer großen Anzahl von Dateien belasten, wenn keine optimale Caching- oder Queuing-Strategie implementiert ist.
  • Bei großen Mengen von SQL-Abfragen kann es zu Synchronisationsproblemen kommen, insbesondere wenn die Abfragen erhebliche Ressourcen erfordern.
  • Das Vorhandensein einer minimalen Fehlerbehandlung bei Sprint-Abfragen erfordert eine zusätzliche Überwachung für eine rechtzeitige Reaktion.

Schlussfolgerung

Die Entwicklung dieses Microservices war für mich eine einzigartige Erfahrung bei der Automatisierung von Routineaufgaben mit Go. Die ordnungsgemäße Trennung der Logik in Konfiguration, Datenbankkonnektivität, Dateiüberwachung und Benachrichtigungen macht das System flexibel und skalierbar. Dieser Ansatz minimiert nicht nur die Risiken, die mit der manuellen Ausführung von SQL-Skripten verbunden sind, sondern ermöglicht auch eine schnelle Reaktion auf Probleme durch Berichte.

Wenn Sie vor ähnlichen Herausforderungen stehen, kann dieses Beispiel ein guter Ausgangspunkt für die Entwicklung einer robusten und benutzerfreundlichen Lösung sein.

package main

import (
	"crypto/tls"
	"database/sql"
	"fmt"
	_ "github.com/octoper/go-ray"
	"log"
	"net/smtp"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Database struct {
		Host     string `yaml:"host"`
		Port     int    `yaml:"port"`
		User     string `yaml:"user"`
		Password string `yaml:"password"`
		Name     string `yaml:"name"`
	} `yaml:"database"`

	SMTP struct {
		Server   string `yaml:"server"`
		Port     int    `yaml:"port"`
		Username string `yaml:"username"`
		Password string `yaml:"password"`
		From     string `yaml:"from"`
		To       string `yaml:"to"`
	} `yaml:"smtp"`

	Paths struct {
		Input  string `yaml:"input"`
		Done   string `yaml:"done"`
		Failed string `yaml:"failed"`
	} `yaml:"paths"`
}

var (
	config Config
	db     *sql.DB
)

func main() {
	loadConfig("config.yml")

	initDB()
	defer db.Close()

	for {
		processFiles()
		time.Sleep(5 * time.Second) // Проверка каждые 5 секунд
	}
}

func loadConfig(filename string) {
	data, err := os.ReadFile(filename)
	if err != nil {
		log.Fatalf("Error reading config file: %v", err)
	}

	err = yaml.Unmarshal(data, &config)
	if err != nil {
		log.Fatalf("Error parsing config file: %v", err)
	}
}

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

func handleSuccess(filePath string) {
	log.Println("SQL executed successfully")
	sendEmail("SQL Execution Success", fmt.Sprintf("File: %s", filePath))
	moveFile(filePath, config.Paths.Done)
}

func moveFile(source, destDir string) {
	fileName := filepath.Base(source)
	destPath := filepath.Join(destDir, fileName)

	err := os.Rename(source, destPath)
	if err != nil {
		log.Printf("Error moving file: %v", err)
	}
}

func sendEmail(subject, body string) {
	tlsConfig := &tls.Config{
		ServerName:         config.SMTP.Server,
		InsecureSkipVerify: false,
	}

	conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTP.Server, config.SMTP.Port), tlsConfig)
	if err != nil {
		log.Printf("Error creating TLS connection: %v", err)
		return
	}
	defer conn.Close()

	client, err := smtp.NewClient(conn, config.SMTP.Server)
	if err != nil {
		log.Printf("Error creating SMTP client: %v", err)
		return
	}
	defer client.Close()

	auth := smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Server)
	if err := client.Auth(auth); err != nil {
		log.Printf("SMTP auth error: %v", err)
		return
	}

	if err := client.Mail(config.SMTP.From); err != nil {
		log.Printf("Mail command error: %v", err)
		return
	}
	if err := client.Rcpt(config.SMTP.To); err != nil {
		log.Printf("Rcpt command error: %v", err)
		return
	}

	wc, err := client.Data()
	if err != nil {
		log.Printf("Data command error: %v", err)
		return
	}
	defer wc.Close()

	msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
		config.SMTP.From,
		config.SMTP.To,
		subject,
		body,
	)

	if _, err = fmt.Fprint(wc, msg); err != nil {
		log.Printf("Error writing message: %v", err)
		return
	}
}