chore: add qe and integrations with rabbit mq

This commit is contained in:
d3m0k1d
2026-04-29 14:26:59 +03:00
parent fe740da06b
commit d3a2fe0f9c
18 changed files with 1823 additions and 47 deletions
@@ -0,0 +1,176 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/skip2/go-qrcode"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/storage"
)
type BaleTypeHandlers struct {
Repo *storage.Repository
}
func (h *BaleTypeHandlers) RegisterRoutes(g *gin.RouterGroup) {
g.GET("/bale-types", h.GetBaleTypes)
g.GET("/bale-types/qr/:id", h.GetBaleTypeQR)
g.GET("/bale-types/:id", h.GetBaleTypeByID)
g.POST("/bale-types", h.CreateBaleType)
g.PUT("/bale-types/:id", h.UpdateBaleType)
g.DELETE("/bale-types/:id", h.DeleteBaleType)
}
// GetBaleTypes Получить список всех типов тюков
// @Summary Получить все типы тюков
// @Description Возвращает список всех типов тюков
// @Tags bale-types
// @Accept json
// @Produce json
// @Success 200 {array} storage.BaleType
// @Router /bale-types [get]
func (h *BaleTypeHandlers) GetBaleTypes(c *gin.Context) {
types, err := h.Repo.GetBaleTypes(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, types)
}
// GetBaleTypeByID Получить тип тюка по ID
// @Summary Получить тип тюка по ID
// @Description Возвращает тип тюка по ID
// @Tags bale-types
// @Accept json
// @Produce json
// @Param id path int true "ID типа тюка"
// @Success 200 {object} storage.BaleType
// @Router /bale-types/{id} [get]
func (h *BaleTypeHandlers) GetBaleTypeByID(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
bt, err := h.Repo.GetBaleTypeByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, bt)
}
// CreateBaleType Создать новый тип тюка
// @Summary Создать тип тюка
// @Description Создаёт новый тип тюка
// @Tags bale-types
// @Accept json
// @Produce json
// @Param bale_type body storage.BaleType true "Данные типа тюка"
// @Success 201 {object} storage.BaleType
// @Router /bale-types [post]
func (h *BaleTypeHandlers) CreateBaleType(c *gin.Context) {
var input storage.BaleType
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bt, err := h.Repo.CreateBaleType(c.Request.Context(), input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, bt)
}
// UpdateBaleType Обновить тип тюка
// @Summary Обновить тип тюка
// @Description Обновляет данные типа тюка по ID
// @Tags bale-types
// @Accept json
// @Produce json
// @Param id path int true "ID типа тюка"
// @Param bale_type body storage.BaleType true "Данные типа тюка"
// @Success 200 {object} storage.BaleType
// @Router /bale-types/{id} [put]
func (h *BaleTypeHandlers) UpdateBaleType(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var input storage.BaleType
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bt, err := h.Repo.UpdateBaleType(c.Request.Context(), id, input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, bt)
}
// DeleteBaleType Удалить тип тюка
// @Summary Удалить тип тюка
// @Description Удаляет тип тюка по ID
// @Tags bale-types
// @Accept json
// @Produce json
// @Param id path int true "ID типа тюка"
// @Success 200 {object} map[string]bool
// @Router /bale-types/{id} [delete]
func (h *BaleTypeHandlers) DeleteBaleType(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.Repo.DeleteBaleType(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deleted": true})
}
// GetBaleTypeQR Получить QR код для маркировки тюка
// @Summary Получить QR код для маркировки тюка
// @Description Возвращает QR код с URL для регистрации тюка этого типа
// @Tags bale-types
// @Accept json
// @Produce png
// @Param id path int true "ID типа тюка"
// @Success 200 {file} image/png
// @Router /bale-types/qr/{id} [get]
func (h *BaleTypeHandlers) GetBaleTypeQR(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
bt, err := h.Repo.GetBaleTypeByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "type not found"})
return
}
server := c.Request.URL.Host
if server == "" {
server = "localhost:8080"
}
url := fmt.Sprintf("http://%s/api/v1/bales?type=%s", server, bt.Type)
png, err := qrcode.Encode(url, qrcode.Medium, 256)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, "image/png", png)
}
+117 -8
View File
@@ -1,15 +1,18 @@
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/mq"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/storage"
)
type BaleHandlers struct {
DB *pgxpool.Pool
Repo *storage.Repository
MQ *mq.RabbitMQ
}
func (h *BaleHandlers) RegisterRoutes(g *gin.RouterGroup) {
@@ -20,22 +23,128 @@ func (h *BaleHandlers) RegisterRoutes(g *gin.RouterGroup) {
g.DELETE("/bales/:id", h.DeleteBale)
}
// GetBales Получить список всех тюков
// @Summary Получить все тюки
// @Description Возвращает список всех тюков
// @Tags bales
// @Accept json
// @Produce json
// @Success 200 {array} storage.Bale
// @Router /bales [get]
func (h *BaleHandlers) GetBales(c *gin.Context) {
c.JSON(200, gin.H{"message": "GetBales"})
bales, err := h.Repo.GetBales(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, bales)
}
// GetBaleByID Получить тюк по ID
// @Summary Получить тюк по ID
// @Description Возвращает тюк по ID
// @Tags bales
// @Accept json
// @Produce json
// @Param id path string true "ID тюка"
// @Success 200 {object} storage.Bale
// @Router /bales/{id} [get]
func (h *BaleHandlers) GetBaleByID(c *gin.Context) {
c.JSON(200, gin.H{"message": "GetBaleByID"})
id := c.Param("id")
bale, err := h.Repo.GetBaleByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, bale)
}
// CreateBale Создать новый тюк
// @Summary Создать тюк
// @Description Создаёт новый тюк и отправляет в очередь задач RabbitMQ
// @Tags bales
// @Accept json
// @Produce json
// @Param bale body storage.Bale false "Данные тюка"
// @Param type query string false "Тип тюка (для QR кодов)"
// @Success 201 {object} storage.Bale
// @Router /bales [post]
func (h *BaleHandlers) CreateBale(c *gin.Context) {
c.JSON(200, gin.H{"message": "CreateBale"})
var input storage.Bale
if err := c.ShouldBindJSON(&input); err != nil && err.Error() != "EOF" {
typeName := c.Query("type")
if typeName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bt, err := h.Repo.GetBaleTypeByType(c.Request.Context(), typeName)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "type not found"})
return
}
input.TypeID = bt.ID
} else if input.TypeID == 0 && c.Query("type") != "" {
typeName := c.Query("type")
bt, err := h.Repo.GetBaleTypeByType(c.Request.Context(), typeName)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "type not found"})
return
}
input.TypeID = bt.ID
}
bale, err := h.Repo.CreateBale(c.Request.Context(), input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if h.MQ != nil {
data, _ := json.Marshal(bale)
h.MQ.Publish(c.Request.Context(), data)
}
c.JSON(http.StatusCreated, bale)
}
// UpdateBale Обновить тюк
// @Summary Обновить тюк
// @Description Обновляет данные тюка по ID
// @Tags bales
// @Accept json
// @Produce json
// @Param id path string true "ID тюка"
// @Param bale body storage.Bale true "Данные тюка"
// @Success 200 {object} storage.Bale
// @Router /bales/{id} [put]
func (h *BaleHandlers) UpdateBale(c *gin.Context) {
c.JSON(200, gin.H{"message": "UpdateBale"})
id := c.Param("id")
var input storage.Bale
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
bale, err := h.Repo.UpdateBale(c.Request.Context(), id, input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, bale)
}
// DeleteBale Удалить тюк
// @Summary Удалить тюк
// @Description Удаляет тюк по ID
// @Tags bales
// @Accept json
// @Produce json
// @Param id path string true "ID тюка"
// @Success 200 {object} map[string]bool
// @Router /bales/{id} [delete]
func (h *BaleHandlers) DeleteBale(c *gin.Context) {
c.JSON(200, gin.H{"message": "DeleteBale"})
id := c.Param("id")
if err := h.Repo.DeleteBale(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deleted": true})
}
@@ -0,0 +1,44 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/mq"
)
type QueueHandlers struct {
MQ *mq.RabbitMQ
}
func (h *QueueHandlers) RegisterRoutes(g *gin.RouterGroup) {
g.GET("/queue/next", h.GetNextTask)
}
// GetNextTask Получить следующую задачу из очереди
// @Summary Получить следующую задачу из очереди
// @Description Получает и удаляет из очереди следующую задачу (FIFO)
// @Tags queue
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Success 404 {object} map[string]string
// @Router /queue/next [get]
func (h *QueueHandlers) GetNextTask(c *gin.Context) {
if h.MQ == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "rabbitmq not available"})
return
}
data, err := h.MQ.Consume()
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "no messages in queue"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": string(data),
"success": true,
})
}
@@ -0,0 +1,64 @@
package handlers
import (
"context"
"testing"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/storage"
)
type mockRepo struct {
storage.Repository
baleTypes []storage.BaleType
bales []storage.Bale
}
func (r *mockRepo) GetBaleTypes(ctx context.Context) ([]storage.BaleType, error) {
return r.baleTypes, nil
}
func (r *mockRepo) GetBales(ctx context.Context) ([]storage.Bale, error) {
return r.bales, nil
}
func TestBaleTypeHandlers_HasRoutes(t *testing.T) {
h := &BaleTypeHandlers{}
if h == nil {
t.Error("BaleTypeHandler is nil")
}
}
func TestBaleHandlers_HasRoutes(t *testing.T) {
h := &BaleHandlers{}
if h == nil {
t.Error("BaleHandler is nil")
}
}
func TestTypesFieldMappings(t *testing.T) {
bt := storage.BaleType{
ID: 1,
Type: "test",
Weight: 10.0,
}
if bt.Type != "test" {
t.Errorf("expected type 'test', got %s", bt.Type)
}
if bt.Weight != 10.0 {
t.Errorf("expected weight 10.0, got %f", bt.Weight)
}
}
func TestBaleFieldMappings(t *testing.T) {
b := storage.Bale{
ID: 1,
TypeID: 1,
Type: "standard",
}
if b.TypeID != 1 {
t.Errorf("expected typeId 1, got %d", b.TypeID)
}
if b.Type != "standard" {
t.Errorf("expected type 'standard', got %s", b.Type)
}
}
+13 -3
View File
@@ -2,20 +2,30 @@ package handlers
import (
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/mq"
"gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/storage"
)
type Handlers struct {
DB *pgxpool.Pool
Repo *storage.Repository
MQ *mq.RabbitMQ
}
func (h *Handlers) RegisterRoutes(r *gin.RouterGroup) {
baleHandlers := &BaleHandlers{
DB: h.DB,
Repo: h.Repo,
MQ: h.MQ,
}
baleHandlers.RegisterRoutes(r)
baleTypeHandlers := &BaleTypeHandlers{
Repo: h.Repo,
}
baleTypeHandlers.RegisterRoutes(r)
queueHandlers := &QueueHandlers{
MQ: h.MQ,
}
queueHandlers.RegisterRoutes(r)
}