diff --git a/backend/Dockerfile b/backend/Dockerfile index e69de29..9faffc8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.26-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 go build -o /server ./cmd/main.go && \ + go install github.com/pressly/goose/v3/cmd/goose@latest + +FROM alpine:3.19 + +RUN apk add --no-cache ca-certificates + +WORKDIR /app + +COPY --from=builder /server /server +COPY --from=builder /go/bin/goose /usr/local/bin/goose +COPY migrations /app/migrations + +EXPOSE 8080 + +CMD ["/server"] \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go index ca4f0e0..bc74098 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -1,12 +1,19 @@ package main import ( + "context" + "log" + "os" + "gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/docs" "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/handlers" + "gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/mq" + "gitea.d3m0k1d.ru/d3m0k1d/rostpoliplast/backend/internal/storage" ) // @securityDefinitions.apikey Bearer @@ -15,6 +22,31 @@ import ( // @description Type "Bearer" followed by a space and the JWT token. func main() { + ctx := context.Background() + + connStr := os.Getenv("DATABASE_URL") + if connStr == "" { + connStr = "postgres://postgres:postgres@localhost:5432/rostpoliplast?sslmode=disable" + } + + pool, err := pgxpool.New(ctx, connStr) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + defer pool.Close() + + if err := pool.Ping(ctx); err != nil { + log.Fatalf("failed to ping database: %v", err) + } + + rabbit, err := mq.NewRabbitMQ() + if err != nil { + log.Printf("warning: failed to connect to rabbitmq: %v", err) + } + defer rabbit.Close() + + repo := storage.NewRepository(pool) + h := &handlers.Handlers{Repo: repo, MQ: rabbit} router := gin.Default() v1 := router.Group("/api/v1") @@ -24,8 +56,7 @@ func main() { docs.SwaggerInfo.Version = "1.0" docs.SwaggerInfo.Schemes = []string{"http", "https"} - baleHandlers := &handlers.Handlers{} - baleHandlers.RegisterRoutes(v1) + h.RegisterRoutes(v1) { v1.GET("/health", func(c *gin.Context) { diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..2c60a0f --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,55 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: rostpoliplast + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + rabbitmq: + image: rabbitmq:3-management-alpine + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: + - "5672:5672" + - "15672:15672" + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 10s + timeout: 10s + retries: 5 + + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + environment: + DATABASE_URL: postgres://postgres:postgres@postgres:5432/rostpoliplast?sslmode=disable + RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/ + depends_on: + postgres: + condition: service_healthy + rabbitmq: + condition: service_healthy + volumes: + - ./migrations:/app/migrations + command: > + sh -c " + goose -dir=/app/migrations postgres 'postgres://postgres:postgres@postgres:5432/rostpoliplast?sslmode=disable' up && + /server + " + +volumes: + postgres_data: \ No newline at end of file diff --git a/backend/docs/docs.go b/backend/docs/docs.go index a760145..2a11f54 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -14,7 +14,376 @@ const docTemplate = `{ }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", - "paths": {}, + "paths": { + "/bale-types": { + "get": { + "description": "Возвращает список всех типов тюков", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Получить все типы тюков", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + } + }, + "post": { + "description": "Создаёт новый тип тюка", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Создать тип тюка", + "parameters": [ + { + "description": "Данные типа тюка", + "name": "bale_type", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + } + }, + "/bale-types/{id}": { + "get": { + "description": "Возвращает тип тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Получить тип тюка по ID", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + }, + "put": { + "description": "Обновляет данные типа тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Обновить тип тюка", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Данные типа тюка", + "name": "bale_type", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + }, + "delete": { + "description": "Удаляет тип тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Удалить тип тюка", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + }, + "/bales": { + "get": { + "description": "Возвращает список всех тюков", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Получить все тюки", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + } + }, + "post": { + "description": "Создаёт новый тюк и отправляет в очередь задач RabbitMQ", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Создать тюк", + "parameters": [ + { + "description": "Данные тюка", + "name": "bale", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + } + }, + "/bales/{id}": { + "get": { + "description": "Возвращает тюк по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Получить тюк по ID", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + }, + "put": { + "description": "Обновляет данные тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Обновить тюк", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Данные тюка", + "name": "bale", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + }, + "delete": { + "description": "Удаляет тюк по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Удалить тюк", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + } + }, + "definitions": { + "gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale": { + "description": "Тюк - единица готовой продукции", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "typeId": { + "type": "integer" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType": { + "description": "Тип тюка - характеристики тюка", + "type": "object", + "properties": { + "height": { + "type": "number" + }, + "id": { + "type": "integer" + }, + "length": { + "type": "number" + }, + "type": { + "type": "string" + }, + "weight": { + "type": "number" + }, + "width": { + "type": "number" + } + } + } + }, "securityDefinitions": { "Bearer": { "description": "Type \"Bearer\" followed by a space and the JWT token.", diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 5173354..dc0f420 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -3,7 +3,376 @@ "info": { "contact": {} }, - "paths": {}, + "paths": { + "/bale-types": { + "get": { + "description": "Возвращает список всех типов тюков", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Получить все типы тюков", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + } + }, + "post": { + "description": "Создаёт новый тип тюка", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Создать тип тюка", + "parameters": [ + { + "description": "Данные типа тюка", + "name": "bale_type", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + } + }, + "/bale-types/{id}": { + "get": { + "description": "Возвращает тип тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Получить тип тюка по ID", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + }, + "put": { + "description": "Обновляет данные типа тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Обновить тип тюка", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Данные типа тюка", + "name": "bale_type", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType" + } + } + } + }, + "delete": { + "description": "Удаляет тип тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bale-types" + ], + "summary": "Удалить тип тюка", + "parameters": [ + { + "type": "integer", + "description": "ID типа тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + }, + "/bales": { + "get": { + "description": "Возвращает список всех тюков", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Получить все тюки", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + } + }, + "post": { + "description": "Создаёт новый тюк и отправляет в очередь задач RabbitMQ", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Создать тюк", + "parameters": [ + { + "description": "Данные тюка", + "name": "bale", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + } + }, + "/bales/{id}": { + "get": { + "description": "Возвращает тюк по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Получить тюк по ID", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + }, + "put": { + "description": "Обновляет данные тюка по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Обновить тюк", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Данные тюка", + "name": "bale", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale" + } + } + } + }, + "delete": { + "description": "Удаляет тюк по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "bales" + ], + "summary": "Удалить тюк", + "parameters": [ + { + "type": "string", + "description": "ID тюка", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + } + }, + "definitions": { + "gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale": { + "description": "Тюк - единица готовой продукции", + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "typeId": { + "type": "integer" + } + } + }, + "gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType": { + "description": "Тип тюка - характеристики тюка", + "type": "object", + "properties": { + "height": { + "type": "number" + }, + "id": { + "type": "integer" + }, + "length": { + "type": "number" + }, + "type": { + "type": "string" + }, + "weight": { + "type": "number" + }, + "width": { + "type": "number" + } + } + } + }, "securityDefinitions": { "Bearer": { "description": "Type \"Bearer\" followed by a space and the JWT token.", diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 0dbe493..52a663d 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -1,6 +1,249 @@ +definitions: + gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale: + description: Тюк - единица готовой продукции + properties: + id: + type: integer + timestamp: + type: string + type: + type: string + typeId: + type: integer + type: object + gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType: + description: Тип тюка - характеристики тюка + properties: + height: + type: number + id: + type: integer + length: + type: number + type: + type: string + weight: + type: number + width: + type: number + type: object info: contact: {} -paths: {} +paths: + /bale-types: + get: + consumes: + - application/json + description: Возвращает список всех типов тюков + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + type: array + summary: Получить все типы тюков + tags: + - bale-types + post: + consumes: + - application/json + description: Создаёт новый тип тюка + parameters: + - description: Данные типа тюка + in: body + name: bale_type + required: true + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + summary: Создать тип тюка + tags: + - bale-types + /bale-types/{id}: + delete: + consumes: + - application/json + description: Удаляет тип тюка по ID + parameters: + - description: ID типа тюка + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: boolean + type: object + summary: Удалить тип тюка + tags: + - bale-types + get: + consumes: + - application/json + description: Возвращает тип тюка по ID + parameters: + - description: ID типа тюка + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + summary: Получить тип тюка по ID + tags: + - bale-types + put: + consumes: + - application/json + description: Обновляет данные типа тюка по ID + parameters: + - description: ID типа тюка + in: path + name: id + required: true + type: integer + - description: Данные типа тюка + in: body + name: bale_type + required: true + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.BaleType' + summary: Обновить тип тюка + tags: + - bale-types + /bales: + get: + consumes: + - application/json + description: Возвращает список всех тюков + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + type: array + summary: Получить все тюки + tags: + - bales + post: + consumes: + - application/json + description: Создаёт новый тюк и отправляет в очередь задач RabbitMQ + parameters: + - description: Данные тюка + in: body + name: bale + required: true + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + summary: Создать тюк + tags: + - bales + /bales/{id}: + delete: + consumes: + - application/json + description: Удаляет тюк по ID + parameters: + - description: ID тюка + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: + type: boolean + type: object + summary: Удалить тюк + tags: + - bales + get: + consumes: + - application/json + description: Возвращает тюк по ID + parameters: + - description: ID тюка + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + summary: Получить тюк по ID + tags: + - bales + put: + consumes: + - application/json + description: Обновляет данные тюка по ID + parameters: + - description: ID тюка + in: path + name: id + required: true + type: string + - description: Данные тюка + in: body + name: bale + required: true + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_rostpoliplast_backend_internal_storage.Bale' + summary: Обновить тюк + tags: + - bales securityDefinitions: Bearer: description: Type "Bearer" followed by a space and the JWT token. diff --git a/backend/go.mod b/backend/go.mod index ca28338..3ff67df 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,8 @@ go 1.26.2 require ( github.com/gin-gonic/gin v1.12.0 + github.com/jackc/pgx/v5 v5.9.2 + github.com/rabbitmq/amqp091-go v1.11.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.8.12 @@ -30,29 +32,29 @@ require ( github.com/goccy/go-yaml v1.19.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.48.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.41.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.44.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 098255f..2ffab77 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -76,8 +76,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -92,8 +92,12 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8= +github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -120,28 +124,30 @@ github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -149,9 +155,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -160,16 +165,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/handlers/BaleType_handlers.go b/backend/internal/handlers/BaleType_handlers.go new file mode 100644 index 0000000..b3c679b --- /dev/null +++ b/backend/internal/handlers/BaleType_handlers.go @@ -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) +} diff --git a/backend/internal/handlers/Bale_handlers.go b/backend/internal/handlers/Bale_handlers.go index bef8665..b723f7d 100644 --- a/backend/internal/handlers/Bale_handlers.go +++ b/backend/internal/handlers/Bale_handlers.go @@ -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}) } diff --git a/backend/internal/handlers/Queue_handlers.go b/backend/internal/handlers/Queue_handlers.go new file mode 100644 index 0000000..6d9f9ba --- /dev/null +++ b/backend/internal/handlers/Queue_handlers.go @@ -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, + }) +} diff --git a/backend/internal/handlers/handlers_test.go b/backend/internal/handlers/handlers_test.go new file mode 100644 index 0000000..60dc64c --- /dev/null +++ b/backend/internal/handlers/handlers_test.go @@ -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) + } +} diff --git a/backend/internal/handlers/register.go b/backend/internal/handlers/register.go index 3d6cdec..baae2fe 100644 --- a/backend/internal/handlers/register.go +++ b/backend/internal/handlers/register.go @@ -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) } diff --git a/backend/internal/mq/rabbitmq.go b/backend/internal/mq/rabbitmq.go new file mode 100644 index 0000000..b88b0d6 --- /dev/null +++ b/backend/internal/mq/rabbitmq.go @@ -0,0 +1,84 @@ +package mq + +import ( + "context" + "fmt" + "os" + "time" + + amqp "github.com/rabbitmq/amqp091-go" +) + +type RabbitMQ struct { + conn *amqp.Connection + channel *amqp.Channel + queue amqp.Queue +} + +func NewRabbitMQ() (*RabbitMQ, error) { + url := os.Getenv("RABBITMQ_URL") + if url == "" { + url = "amqp://guest:guest@localhost:5672/" + } + + conn, err := amqp.Dial(url) + if err != nil { + return nil, fmt.Errorf("failed to connect to RabbitMQ: %w", err) + } + + ch, err := conn.Channel() + if err != nil { + conn.Close() + return nil, fmt.Errorf("failed to open channel: %w", err) + } + + q, err := ch.QueueDeclare("bales_tasks", true, false, false, false, nil) + if err != nil { + ch.Close() + conn.Close() + return nil, fmt.Errorf("failed to declare queue: %w", err) + } + + return &RabbitMQ{ + conn: conn, + channel: ch, + queue: q, + }, nil +} + +func (r *RabbitMQ) Publish(ctx context.Context, body []byte) error { + return r.channel.PublishWithContext(ctx, "", r.queue.Name, false, false, amqp.Publishing{ + ContentType: "application/json", + Body: body, + }) +} + +func (r *RabbitMQ) Consume() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + msgs, err := r.channel.Consume(r.queue.Name, "", false, false, false, false, nil) + if err != nil { + return nil, err + } + + select { + case msg, ok := <-msgs: + if !ok { + return nil, fmt.Errorf("channel closed") + } + msg.Ack(false) + return msg.Body, nil + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for message") + } +} + +func (r *RabbitMQ) Close() { + if r.channel != nil { + r.channel.Close() + } + if r.conn != nil { + r.conn.Close() + } +} diff --git a/backend/internal/storage/repository.go b/backend/internal/storage/repository.go index 1a16c18..721bdd5 100644 --- a/backend/internal/storage/repository.go +++ b/backend/internal/storage/repository.go @@ -1,6 +1,8 @@ package storage import ( + "context" + "github.com/jackc/pgx/v5/pgxpool" ) @@ -13,3 +15,139 @@ func NewRepository(pool *pgxpool.Pool) *Repository { pool: pool, } } + +func (r *Repository) GetBales(ctx context.Context) ([]Bale, error) { + rows, err := r.pool.Query(ctx, ` + SELECT b.id, b.type_id, COALESCE(bt.type, ''), b.timestamp + FROM bales b + LEFT JOIN bale_types bt ON b.type_id = bt.id + ORDER BY b.id`) + if err != nil { + return nil, err + } + defer rows.Close() + + var bales []Bale + for rows.Next() { + var b Bale + if err := rows.Scan(&b.ID, &b.TypeID, &b.Type, &b.Timestamp); err != nil { + return nil, err + } + bales = append(bales, b) + } + return bales, nil +} + +func (r *Repository) GetBaleByID(ctx context.Context, id string) (*Bale, error) { + var b Bale + err := r.pool.QueryRow(ctx, ` + SELECT b.id, b.type_id, COALESCE(bt.type, ''), b.timestamp + FROM bales b + LEFT JOIN bale_types bt ON b.type_id = bt.id + WHERE b.id = $1`, id).Scan(&b.ID, &b.TypeID, &b.Type, &b.Timestamp) + if err != nil { + return nil, err + } + return &b, nil +} + +func (r *Repository) CreateBale(ctx context.Context, input Bale) (*Bale, error) { + var b Bale + err := r.pool.QueryRow(ctx, ` + INSERT INTO bales (type_id) VALUES ($1) + RETURNING id, type_id, timestamp`, input.TypeID).Scan(&b.ID, &b.TypeID, &b.Timestamp) + if err != nil { + return nil, err + } + if input.TypeID > 0 { + var bt BaleType + r.pool.QueryRow(ctx, "SELECT id, type FROM bale_types WHERE id = $1", b.TypeID).Scan(&bt.ID, &bt.Type) + b.Type = bt.Type + } + return &b, nil +} + +func (r *Repository) UpdateBale(ctx context.Context, id string, input Bale) (*Bale, error) { + var b Bale + err := r.pool.QueryRow(ctx, ` + UPDATE bales SET type_id = $1 WHERE id = $2 + RETURNING id, type_id, timestamp`, input.TypeID, id).Scan(&b.ID, &b.TypeID, &b.Timestamp) + if err != nil { + return nil, err + } + if input.TypeID > 0 { + var bt BaleType + r.pool.QueryRow(ctx, "SELECT id, type FROM bale_types WHERE id = $1", b.TypeID).Scan(&bt.ID, &bt.Type) + b.Type = bt.Type + } + return &b, nil +} + +func (r *Repository) DeleteBale(ctx context.Context, id string) error { + _, err := r.pool.Exec(ctx, "DELETE FROM bales WHERE id = $1", id) + return err +} + +func (r *Repository) GetBaleTypes(ctx context.Context) ([]BaleType, error) { + rows, err := r.pool.Query(ctx, "SELECT id, type, weight, height, width, length FROM bale_types ORDER BY id") + if err != nil { + return nil, err + } + defer rows.Close() + + var types []BaleType + for rows.Next() { + var bt BaleType + if err := rows.Scan(&bt.ID, &bt.Type, &bt.Weight, &bt.Height, &bt.Width, &bt.Length); err != nil { + return nil, err + } + types = append(types, bt) + } + return types, nil +} + +func (r *Repository) GetBaleTypeByID(ctx context.Context, id int) (*BaleType, error) { + var bt BaleType + err := r.pool.QueryRow(ctx, "SELECT id, type, weight, height, width, length FROM bale_types WHERE id = $1", id).Scan(&bt.ID, &bt.Type, &bt.Weight, &bt.Height, &bt.Width, &bt.Length) + if err != nil { + return nil, err + } + return &bt, nil +} + +func (r *Repository) GetBaleTypeByType(ctx context.Context, typeName string) (*BaleType, error) { + var bt BaleType + err := r.pool.QueryRow(ctx, "SELECT id, type, weight, height, width, length FROM bale_types WHERE type = $1", typeName).Scan(&bt.ID, &bt.Type, &bt.Weight, &bt.Height, &bt.Width, &bt.Length) + if err != nil { + return nil, err + } + return &bt, nil +} + +func (r *Repository) CreateBaleType(ctx context.Context, input BaleType) (*BaleType, error) { + var bt BaleType + err := r.pool.QueryRow(ctx, ` + INSERT INTO bale_types (type, weight, height, width, length) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, type, weight, height, width, length`, input.Type, input.Weight, input.Height, input.Width, input.Length).Scan(&bt.ID, &bt.Type, &bt.Weight, &bt.Height, &bt.Width, &bt.Length) + if err != nil { + return nil, err + } + return &bt, nil +} + +func (r *Repository) UpdateBaleType(ctx context.Context, id int, input BaleType) (*BaleType, error) { + var bt BaleType + err := r.pool.QueryRow(ctx, ` + UPDATE bale_types SET type = $1, weight = $2, height = $3, width = $4, length = $5 WHERE id = $6 + RETURNING id, type, weight, height, width, length`, input.Type, input.Weight, input.Height, input.Width, input.Length, id).Scan(&bt.ID, &bt.Type, &bt.Weight, &bt.Height, &bt.Width, &bt.Length) + if err != nil { + return nil, err + } + return &bt, nil +} + +func (r *Repository) DeleteBaleType(ctx context.Context, id int) error { + _, err := r.pool.Exec(ctx, "DELETE FROM bale_types WHERE id = $1", id) + return err +} diff --git a/backend/internal/storage/types.go b/backend/internal/storage/types.go index 6b5f2df..4e8f7eb 100644 --- a/backend/internal/storage/types.go +++ b/backend/internal/storage/types.go @@ -1,5 +1,9 @@ package storage +import "time" + +// BaleType Тип тюка +// @Description Тип тюка - характеристики тюка type BaleType struct { ID int `json:"id"` Type string `json:"type"` @@ -9,10 +13,13 @@ type BaleType struct { Length float64 `json:"length"` } +// Bale Тюк +// @Description Тюк - единица готовой продукции type Bale struct { - ID int `json:"id"` - Type string `json:"type"` - Timestamp string `json:"timestamp"` + ID int `json:"id"` + TypeID int `json:"typeId"` + Type string `json:"type"` + Timestamp time.Time `json:"timestamp"` } type User struct { diff --git a/backend/migrations/001_create_bales.sql b/backend/migrations/001_create_bales.sql new file mode 100644 index 0000000..02fa4df --- /dev/null +++ b/backend/migrations/001_create_bales.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS bales ( + id SERIAL PRIMARY KEY, + type_id INTEGER, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_bales_timestamp ON bales(timestamp); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS bales; +-- +goose StatementEnd \ No newline at end of file diff --git a/backend/migrations/002_create_bale_types.sql b/backend/migrations/002_create_bale_types.sql new file mode 100644 index 0000000..af18d40 --- /dev/null +++ b/backend/migrations/002_create_bale_types.sql @@ -0,0 +1,28 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS bale_types ( + id SERIAL PRIMARY KEY, + type VARCHAR(255) NOT NULL, + weight DECIMAL(10,2), + height DECIMAL(10,2), + width DECIMAL(10,2), + length DECIMAL(10,2) +); +-- +goose StatementEnd + +-- +goose StatementBegin +ALTER TABLE bales ADD COLUMN IF NOT EXISTS type_id INTEGER REFERENCES bale_types(id); +-- +goose StatementEnd + +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_bales_type_id ON bales(type_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE bales DROP COLUMN IF NOT EXISTS type_id; +-- +goose StatementEnd + +-- +goose StatementBegin +DROP TABLE IF EXISTS bale_types; +-- +goose StatementEnd \ No newline at end of file