Compare commits

...

30 commits

Author SHA1 Message Date
nquidox
8f2b0470b1 false zero price bugfix
All checks were successful
/ Make image (push) Successful in 1m3s
2025-12-07 13:37:15 +03:00
nquidox
4c59ab3f58 return origin name instead of code
All checks were successful
/ Make image (push) Successful in 1m15s
2025-12-06 19:14:21 +03:00
nquidox
f9eac067be swagger docs update 2025-12-06 17:33:29 +03:00
nquidox
d997d8bfa4 added: delete zero prices in period 2025-12-06 17:33:18 +03:00
nquidox
a338fd03b2 added: time util 2025-12-06 17:32:53 +03:00
nquidox
aeb5cb819b сhanged ownership validation for user's merch
All checks were successful
/ Make image (push) Successful in 1m30s
2025-11-04 16:49:04 +03:00
nquidox
7fa79d770a swagger docs update
All checks were successful
/ Make image (push) Successful in 1m3s
2025-11-02 23:39:25 +03:00
nquidox
2728051fde fixes 2025-11-02 23:27:23 +03:00
nquidox
a0e21db5a0 swagger docs update 2025-11-02 21:10:59 +03:00
nquidox
93ce93770d zero prices check added 2025-11-02 21:10:49 +03:00
nquidox
88fcbfe1a5 routes + repo fix 2025-11-02 20:59:25 +03:00
nquidox
8186d8a46c getMerchLabels + fixes
All checks were successful
/ Make image (push) Successful in 1m3s
2025-10-29 20:56:26 +03:00
nquidox
b6f7875710 update 2025-10-29 20:55:51 +03:00
nquidox
8ac753f632 labels added to dto 2025-10-28 21:46:40 +03:00
nquidox
565e019a67 swagger docs update 2025-10-28 20:30:05 +03:00
nquidox
f7ec1bce1e small fixes 2025-10-28 20:29:14 +03:00
nquidox
844561ef70 update 2025-10-28 20:28:40 +03:00
nquidox
9895b86666 labels crud added 2025-10-28 20:06:32 +03:00
nquidox
475ff9919b tables added 2025-10-28 18:22:40 +03:00
nquidox
489f749ce3 minio disabled
All checks were successful
/ Make image (push) Successful in 1m8s
2025-10-26 21:59:49 +03:00
nquidox
7937d182db swagger docs update
All checks were successful
/ Make image (push) Successful in 1m9s
2025-10-26 19:55:49 +03:00
nquidox
37a1dfbf52 swagger docs update 2025-10-26 19:54:51 +03:00
nquidox
f13012b742 update 2025-10-26 19:54:43 +03:00
nquidox
212ce0a5c4 image storage added 2025-10-26 19:54:34 +03:00
nquidox
f5ca21ca68 deprecated comment 2025-10-26 19:54:10 +03:00
nquidox
fa8990ed8c new image provider 2025-10-26 19:53:49 +03:00
nquidox
a3fbd3b8e0 error handling 2025-10-26 19:53:32 +03:00
nquidox
e90852cc95 factor out tp methods 2025-10-26 19:53:14 +03:00
nquidox
dae627f4ad image storage contract added 2025-10-26 19:52:46 +03:00
nquidox
3298602a23 switch from pre-signed to public images
All checks were successful
/ Make image (push) Successful in 1m31s
2025-10-19 19:43:33 +03:00
29 changed files with 3144 additions and 250 deletions

View file

@ -8,6 +8,9 @@ APP_ALLOWED_ORIGINS=http://localhost:5173,
GRPC_SERVER_PORT=9050
GRPC_CLIENT_PORT=9060
IMAGE_STORAGE_HOST=
IMAGE_STORAGE_PORT=
MEDIA_STORAGE_ENDPOINT=
MEDIA_STORAGE_USER=
MEDIA_STORAGE_PASSWORD=

View file

@ -9,6 +9,7 @@ import (
"merch-parser-api/internal/api/user"
"merch-parser-api/internal/app"
"merch-parser-api/internal/grpcService"
"merch-parser-api/internal/imagesProvider"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/mediaStorage"
"merch-parser-api/internal/provider/auth"
@ -62,6 +63,8 @@ func main() {
"provider": mediaProvider,
}).Debug("Media storage | Minio client created")
imageProvider := imagesProvider.NewClient(c.ImageConf.Host + ":" + c.ImageConf.Port)
//deps providers
routerHandler := router.NewRouter(router.Deps{
ApiPrefix: c.AppConf.ApiPrefix,
@ -93,7 +96,8 @@ func main() {
merchModule := merch.NewHandler(merch.Deps{
DB: database,
Utils: utilsProvider,
Media: mediaProvider,
//Media: mediaProvider,
ImageStorage: imageProvider,
})
//collect modules

View file

@ -8,6 +8,7 @@ type Config struct {
JWTConf JWTConfig
GrpcConf GrpcConfig
MediaConf MediaConfig
ImageConf ImageStorageConfig
}
type AppConfig struct {
@ -48,6 +49,11 @@ type MediaConfig struct {
Secure string
}
type ImageStorageConfig struct {
Host string
Port string
}
func NewConfig() *Config {
return &Config{
AppConf: AppConfig{
@ -87,5 +93,10 @@ func NewConfig() *Config {
Password: getEnv("MEDIA_STORAGE_PASSWORD", ""),
Secure: getEnv("MEDIA_STORAGE_SECURE", ""),
},
ImageConf: ImageStorageConfig{
Host: getEnv("IMAGE_STORAGE_HOST", ""),
Port: getEnv("IMAGE_STORAGE_PORT", ""),
},
}
}

View file

@ -68,6 +68,9 @@ const docTemplate = `{
}
],
"description": "Получить все записи мерча",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -103,6 +106,9 @@ const docTemplate = `{
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
@ -145,6 +151,9 @@ const docTemplate = `{
}
],
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
@ -192,7 +201,7 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "Загрузить картинки по merch_uuid и query параметрам",
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
@ -202,7 +211,7 @@ const docTemplate = `{
"tags": [
"Merch images"
],
"summary": "Загрузить картинки по merch_uuid и query параметрам",
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
@ -217,18 +226,313 @@ const docTemplate = `{
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/imageStorage.UploadMerchImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid",
"tags": [
"Merch images"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить все метки товаров",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить все метки товаров",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Создать новую метку для товара",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Создать новую метку для товара",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/attach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Прикрепить метку к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Прикрепить метку к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/detach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить привязку метки к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Удалить привязку метки к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить метки товара по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить метки товара по его uuid",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Изменить метку",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"enum": [
"thumbnail",
"full",
"all"
],
"type": "string",
"description": "Image type: thumbnail, full or all",
"name": "imageType",
"in": "formData",
"required": true
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
@ -255,15 +559,15 @@ const docTemplate = `{
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid и query параметрам",
"description": "Пометить метку как удаленную",
"tags": [
"Merch images"
"Merch labels"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid и query параметрам",
"summary": "Пометить метку как удаленную",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
@ -288,6 +592,136 @@ const docTemplate = `{
}
}
},
"/merch/zeroprices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить нулевые цены",
"produces": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Получить нулевые цены",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.ZeroPrice"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные",
"consumes": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.DeleteZeroPrices"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices/period": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные за указанный период",
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные за указанный период",
"parameters": [
{
"type": "string",
"description": "start",
"name": "start",
"in": "query",
"required": true
},
{
"type": "string",
"description": "end",
"name": "end",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"get": {
"security": [
@ -296,6 +730,9 @@ const docTemplate = `{
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -380,6 +817,9 @@ const docTemplate = `{
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -425,6 +865,9 @@ const docTemplate = `{
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -768,6 +1211,28 @@ const docTemplate = `{
}
},
"definitions": {
"imageStorage.UploadMerchImageResponse": {
"type": "object",
"properties": {
"fullImage": {
"type": "string"
},
"thumbnail": {
"type": "string"
}
}
},
"merch.DeleteZeroPrices": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.ImageLink": {
"type": "object",
"properties": {
@ -779,9 +1244,57 @@ const docTemplate = `{
}
}
},
"merch.LabelDTO": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.LabelLink": {
"type": "object",
"properties": {
"label_uuid": {
"type": "string"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.LabelsList": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"label_uuid": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.ListResponse": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -801,6 +1314,12 @@ const docTemplate = `{
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -882,6 +1401,26 @@ const docTemplate = `{
}
}
},
"merch.ZeroPrice": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"responses.ErrorResponse400": {
"type": "object",
"properties": {

View file

@ -60,6 +60,9 @@
}
],
"description": "Получить все записи мерча",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -95,6 +98,9 @@
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
@ -137,6 +143,9 @@
}
],
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
@ -184,7 +193,7 @@
"BearerAuth": []
}
],
"description": "Загрузить картинки по merch_uuid и query параметрам",
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
@ -194,7 +203,7 @@
"tags": [
"Merch images"
],
"summary": "Загрузить картинки по merch_uuid и query параметрам",
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
@ -209,18 +218,313 @@
"name": "file",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/imageStorage.UploadMerchImageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid",
"tags": [
"Merch images"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить все метки товаров",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить все метки товаров",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Создать новую метку для товара",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Создать новую метку для товара",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/attach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Прикрепить метку к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Прикрепить метку к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/detach": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Удалить привязку метки к товару",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Удалить привязку метки к товару",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelLink"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/labels/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить метки товара по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Получить метки товара по его uuid",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Изменить метку",
"consumes": [
"application/json"
],
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"enum": [
"thumbnail",
"full",
"all"
],
"type": "string",
"description": "Image type: thumbnail, full or all",
"name": "imageType",
"in": "formData",
"required": true
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.LabelDTO"
}
}
],
"responses": {
@ -247,15 +551,15 @@
"BearerAuth": []
}
],
"description": "Удалить (безвозвратно) картинки по merch_uuid и query параметрам",
"description": "Пометить метку как удаленную",
"tags": [
"Merch images"
"Merch labels"
],
"summary": "Удалить (безвозвратно) картинки по merch_uuid и query параметрам",
"summary": "Пометить метку как удаленную",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
@ -280,6 +584,136 @@
}
}
},
"/merch/zeroprices": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить нулевые цены",
"produces": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Получить нулевые цены",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/merch.ZeroPrice"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные",
"consumes": [
"application/json"
],
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные",
"parameters": [
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.DeleteZeroPrices"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/zeroprices/period": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить нулевые цены как удаленные за указанный период",
"tags": [
"Merch zero prices"
],
"summary": "Пометить нулевые цены как удаленные за указанный период",
"parameters": [
{
"type": "string",
"description": "start",
"name": "start",
"in": "query",
"required": true
},
{
"type": "string",
"description": "end",
"name": "end",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
}
},
"/merch/{uuid}": {
"get": {
"security": [
@ -288,6 +722,9 @@
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -372,6 +809,9 @@
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -417,6 +857,9 @@
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -760,6 +1203,28 @@
}
},
"definitions": {
"imageStorage.UploadMerchImageResponse": {
"type": "object",
"properties": {
"fullImage": {
"type": "string"
},
"thumbnail": {
"type": "string"
}
}
},
"merch.DeleteZeroPrices": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.ImageLink": {
"type": "object",
"properties": {
@ -771,9 +1236,57 @@
}
}
},
"merch.LabelDTO": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.LabelLink": {
"type": "object",
"properties": {
"label_uuid": {
"type": "string"
},
"merch_uuid": {
"type": "string"
}
}
},
"merch.LabelsList": {
"type": "object",
"properties": {
"bg_color": {
"type": "string"
},
"color": {
"type": "string"
},
"label_uuid": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"merch.ListResponse": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -793,6 +1306,12 @@
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -874,6 +1393,26 @@
}
}
},
"merch.ZeroPrice": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"responses.ErrorResponse400": {
"type": "object",
"properties": {

View file

@ -1,5 +1,19 @@
basePath: /api/v2
definitions:
imageStorage.UploadMerchImageResponse:
properties:
fullImage:
type: string
thumbnail:
type: string
type: object
merch.DeleteZeroPrices:
properties:
id:
type: integer
merch_uuid:
type: string
type: object
merch.ImageLink:
properties:
etag:
@ -7,8 +21,39 @@ definitions:
link:
type: string
type: object
merch.LabelDTO:
properties:
bg_color:
type: string
color:
type: string
name:
type: string
type: object
merch.LabelLink:
properties:
label_uuid:
type: string
merch_uuid:
type: string
type: object
merch.LabelsList:
properties:
bg_color:
type: string
color:
type: string
label_uuid:
type: string
name:
type: string
type: object
merch.ListResponse:
properties:
labels:
items:
type: string
type: array
merch_uuid:
type: string
name:
@ -21,6 +66,10 @@ definitions:
type: object
merch.MerchDTO:
properties:
labels:
items:
type: string
type: array
merch_uuid:
type: string
name:
@ -73,6 +122,19 @@ definitions:
origin:
type: string
type: object
merch.ZeroPrice:
properties:
created_at:
type: string
id:
type: integer
merch_uuid:
type: string
name:
type: string
origin:
type: string
type: object
responses.ErrorResponse400:
properties:
error:
@ -171,6 +233,8 @@ paths:
/merch/:
get:
description: Получить все записи мерча
produces:
- application/json
responses:
"200":
description: OK
@ -192,6 +256,8 @@ paths:
tags:
- Merch
put:
consumes:
- application/json
description: Обновить информацию про мерч по его uuid в json-е
parameters:
- description: merch_uuid
@ -251,6 +317,8 @@ paths:
name: uuid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -271,7 +339,7 @@ paths:
- Merch
/merch/images/{uuid}:
delete:
description: Удалить (безвозвратно) картинки по merch_uuid и query параметрам
description: Удалить (безвозвратно) картинки по merch_uuid
parameters:
- description: merch_uuid
in: path
@ -291,7 +359,7 @@ paths:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Удалить (безвозвратно) картинки по merch_uuid и query параметрам
summary: Удалить (безвозвратно) картинки по merch_uuid
tags:
- Merch images
get:
@ -307,6 +375,8 @@ paths:
name: type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -328,7 +398,8 @@ paths:
post:
consumes:
- multipart/form-data
description: Загрузить картинки по merch_uuid и query параметрам
description: Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на
созданные картинки.
parameters:
- description: Merch UUID
in: path
@ -340,13 +411,109 @@ paths:
name: file
required: true
type: file
- description: 'Image type: thumbnail, full or all'
enum:
- thumbnail
- full
- all
in: formData
name: imageType
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/imageStorage.UploadMerchImageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Загрузить картинку по merch_uuid
tags:
- Merch images
/merch/labels:
get:
description: Получить все метки товаров
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/merch.LabelsList'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить все метки товаров
tags:
- Merch labels
post:
consumes:
- application/json
description: Создать новую метку для товара
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelDTO'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Создать новую метку для товара
tags:
- Merch labels
/merch/labels/{uuid}:
delete:
description: Пометить метку как удаленную
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить метку как удаленную
tags:
- Merch labels
get:
description: Получить метки товара по его uuid
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
produces:
@ -364,9 +531,179 @@ paths:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Загрузить картинки по merch_uuid и query параметрам
summary: Получить метки товара по его uuid
tags:
- Merch images
- Merch labels
put:
consumes:
- application/json
description: Изменить метку
parameters:
- description: label uuid
in: path
name: uuid
required: true
type: string
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelDTO'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Изменить метку
tags:
- Merch labels
/merch/labels/attach:
post:
consumes:
- application/json
description: Прикрепить метку к товару
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelLink'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Прикрепить метку к товару
tags:
- Merch labels
/merch/labels/detach:
post:
consumes:
- application/json
description: Удалить привязку метки к товару
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.LabelLink'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Удалить привязку метки к товару
tags:
- Merch labels
/merch/zeroprices:
delete:
consumes:
- application/json
description: Пометить нулевые цены как удаленные
parameters:
- description: payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/merch.DeleteZeroPrices'
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить нулевые цены как удаленные
tags:
- Merch zero prices
get:
description: Получить нулевые цены
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/merch.ZeroPrice'
type: array
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить нулевые цены
tags:
- Merch zero prices
/merch/zeroprices/period:
delete:
description: Пометить нулевые цены как удаленные за указанный период
parameters:
- description: start
in: query
name: start
required: true
type: string
- description: end
in: query
name: end
required: true
type: string
responses:
"200":
description: OK
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Пометить нулевые цены как удаленные за указанный период
tags:
- Merch zero prices
/prices:
get:
description: Получить цены мерча за период
@ -375,6 +712,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -408,6 +747,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK

4
go.mod
View file

@ -52,7 +52,7 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -79,5 +79,5 @@ require (
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
)

8
go.sum
View file

@ -89,8 +89,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@ -217,8 +217,8 @@ golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View file

@ -27,7 +27,6 @@ func newController(service *service, utils interfaces.Utils, expires time.Durati
func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, refreshMW gin.HandlerFunc) {
merchGroup := r.Group("/merch", authMW)
merchGroup.POST("/", h.controller.addMerch)
merchGroup.GET("/:uuid", h.controller.getSingleMerch)
merchGroup.GET("/", h.controller.getAllMerch)
@ -42,6 +41,22 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref
imagesGroup.POST("/:uuid", h.controller.uploadMerchImage)
imagesGroup.GET("/:uuid", h.controller.getMerchImage)
imagesGroup.DELETE("/:uuid", h.controller.deleteMerchImage)
labelsGroup := merchGroup.Group("/labels", authMW)
labelsGroup.POST("", h.controller.createLabel)
labelsGroup.GET("", h.controller.getLabels)
labelsGroup.PUT("/:uuid", h.controller.updateLabel)
labelsGroup.DELETE("/:uuid", h.controller.deleteLabel)
labelsGroup.POST("/attach", h.controller.attachLabel)
labelsGroup.POST("/detach", h.controller.detachLabel)
labelsGroup.GET("/:uuid", h.controller.getMerchLabels)
zeroPricesGroup := merchGroup.Group("/zeroprices", authMW)
zeroPricesGroup.GET("", h.controller.getZeroPrices)
zeroPricesGroup.DELETE("", h.controller.deleteZeroPrices)
zeroPricesGroup.DELETE("/period", h.controller.deleteZeroPricesPeriod)
}
// @Summary Добавить новый мерч
@ -83,6 +98,7 @@ func (co *controller) addMerch(c *gin.Context) {
// @Description Получить всю информацию про мерч по его uuid
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Success 200 {object} MerchDTO
// @Failure 400 {object} responses.ErrorResponse400
@ -116,6 +132,7 @@ func (co *controller) getSingleMerch(c *gin.Context) {
// @Description Получить все записи мерча
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Success 200 {array} ListResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
@ -142,6 +159,7 @@ func (co *controller) getAllMerch(c *gin.Context) {
// @Description Обновить информацию про мерч по его uuid в json-е
// @Tags Merch
// @Security BearerAuth
// @Accept json
// @Param body body UpdateMerchDTO true "merch_uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
@ -167,6 +185,7 @@ func (co *controller) updateMerch(c *gin.Context) {
log.WithError(err).Error("Merch | Failed to get single merch")
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить мерч как удаленный
@ -177,6 +196,7 @@ func (co *controller) updateMerch(c *gin.Context) {
// @Success 200 {object} MerchDTO
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
//
// @Router /merch/{uuid} [delete]
func (co *controller) deleteMerch(c *gin.Context) {
merchUuid := c.Param("uuid")
@ -205,11 +225,16 @@ func (co *controller) deleteMerch(c *gin.Context) {
// @Description Получить цены мерча за период
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param days query string false "period in days"
// @Success 200 {array} PricesResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices [get]
//
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices [get]
func (co *controller) getChartsPrices(c *gin.Context) {
daysQuery := strings.ToLower(c.DefaultQuery("days", ""))
@ -234,6 +259,7 @@ func (co *controller) getChartsPrices(c *gin.Context) {
// @Description Получить перепады цен мерча за период по его merch_uuid
// @Tags Merch
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Param days query string false "period in days"
// @Success 200 {object} PricesResponse
@ -266,16 +292,15 @@ func (co *controller) getDistinctPrices(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// @Summary Загрузить картинки по merch_uuid и query параметрам
// @Description Загрузить картинки по merch_uuid и query параметрам
// @Summary Загрузить картинку по merch_uuid
// @Description Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.
// @Tags Merch images
// @Security BearerAuth
// @Accept multipart/form-data
// @Produce json
// @Param uuid path string true "Merch UUID"
// @Param file formData file true "Image file"
// @Param imageType formData string true "Image type: thumbnail, full or all" Enums(thumbnail, full, all)
// @Success 200
// @Success 200 {object} imageStorage.UploadMerchImageResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [post]
@ -294,13 +319,14 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
return
}
imageType := c.PostForm("imageType")
types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}}
if _, allowed := types[imageType]; !allowed {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"})
log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all")
return
}
//Uncomment for MinIO use
//imageType := c.PostForm("imageType")
//types := map[string]struct{}{"thumbnail": {}, "full": {}, "all": {}}
//if _, allowed := types[imageType]; !allowed {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "imageType must be one of: thumbnail, full, all"})
// log.WithError(err).Error("Merch | imageType must be one of: thumbnail, full, all")
// return
//}
file, err := c.FormFile("file")
if err != nil {
@ -312,20 +338,23 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file)
//Uncomment for MinIO use
//err = co.service.uploadMerchImage(ctx, userUuid, merchUuid, imageType, file)
response, err := co.service.mtUploadMerchImage(ctx, userUuid, merchUuid, file)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to upload merch image")
return
}
c.Status(http.StatusOK)
c.JSON(http.StatusOK, response)
}
// @Summary Получить картинки по merch_uuid и query параметрам
// @Description Получить картинки по merch_uuid и query параметрам
// @Tags Merch images
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "merch_uuid"
// @Param type query string true "image type"
// @Success 200 {object} ImageLink
@ -333,40 +362,49 @@ func (co *controller) uploadMerchImage(c *gin.Context) {
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [get]
func (co *controller) getMerchImage(c *gin.Context) {
typeQuery := strings.ToLower(c.Query("type"))
if typeQuery == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
return
//Uncomment for MinIO use
//typeQuery := strings.ToLower(c.Query("type"))
//if typeQuery == "" {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "Image type query param is empty"})
// return
//}
//
//userUuid, err := co.utils.GetUserUuidFromContext(c)
//if err != nil {
// c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
// log.WithError(err).Error("Merch | Failed to get user uuid from context")
// return
//}
//
//merchUuid := c.Param("uuid")
//if merchUuid == "" {
// c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
// log.WithError(err).Error("Merch | Failed to get single merch")
// return
//}
//
//ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
//defer cancel()
//
//link, err := co.service.getPublicImageLink(ctx, userUuid, merchUuid, typeQuery)
//if err != nil {
// c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
// log.WithError(err).Error("Merch | Failed to get merch image")
// return
//}
//
//if link.Link == "" {
// log.Debug("Merch | No image")
// c.Status(http.StatusNoContent)
// return
//}
//
//c.JSON(http.StatusOK, link)
}
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to get user uuid from context")
return
}
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
log.WithError(err).Error("Merch | Failed to get single merch")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
link, err := co.service.getMerchImage(ctx, userUuid, merchUuid, typeQuery)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to get merch image")
return
}
c.JSON(http.StatusOK, link)
}
// @Summary Удалить (безвозвратно) картинки по merch_uuid и query параметрам
// @Description Удалить (безвозвратно) картинки по merch_uuid и query параметрам
// @Summary Удалить (безвозвратно) картинки по merch_uuid
// @Description Удалить (безвозвратно) картинки по merch_uuid
// @Tags Merch images
// @Security BearerAuth
// @Param uuid path string true "merch_uuid"
@ -385,17 +423,374 @@ func (co *controller) deleteMerchImage(c *gin.Context) {
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "MerchUuid is empty"})
log.WithError(err).Error("Merch | Failed to get single merch")
log.WithError(err).Error("Merch | Failed to get merch uuid")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
//Uncomment for MinIO use
//if err := co.service.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
if err := co.service.mtDeleteMerchImage(ctx, userUuid, merchUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error("Merch | Failed to delete merch image")
return
}
c.Status(http.StatusOK)
}
// @Summary Создать новую метку для товара
// @Description Создать новую метку для товара
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelDTO true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels [post]
func (co *controller) createLabel(c *gin.Context) {
const logMsg = "Merch | Create label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelDTO
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.createLabel(payload, userUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Получить все метки товаров
// @Description Получить все метки товаров
// @Tags Merch labels
// @Security BearerAuth
// @Produce json
// @Success 200 {array} LabelsList
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels [get]
func (co *controller) getLabels(c *gin.Context) {
const logMsg = "Merch | Get labels"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getLabels(userUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Изменить метку
// @Description Изменить метку
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param uuid path string true "label uuid"
// @Param payload body LabelDTO true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [put]
func (co *controller) updateLabel(c *gin.Context) {
const logMsg = "Merch | Update label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
labelUuid := c.Param("uuid")
if labelUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
var payload LabelDTO
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.updateLabel(userUuid, labelUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить метку как удаленную
// @Description Пометить метку как удаленную
// @Tags Merch labels
// @Security BearerAuth
// @Param uuid path string true "label uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [delete]
func (co *controller) deleteLabel(c *gin.Context) {
const logMsg = "Merch | Delete label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
labelUuid := c.Param("uuid")
if labelUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteLabel(userUuid, labelUuid); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Прикрепить метку к товару
// @Description Прикрепить метку к товару
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelLink true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/attach [post]
func (co *controller) attachLabel(c *gin.Context) {
const logMsg = "Merch | Attach label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelLink
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.attachLabel(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Удалить привязку метки к товару
// @Description Удалить привязку метки к товару
// @Tags Merch labels
// @Security BearerAuth
// @Accept json
// @Param payload body LabelLink true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/detach [post]
func (co *controller) detachLabel(c *gin.Context) {
const logMsg = "Merch | Detach label"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload LabelLink
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.detachLabel(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Получить метки товара по его uuid
// @Description Получить метки товара по его uuid
// @Tags Merch labels
// @Security BearerAuth
// @Produce json
// @Param uuid path string true "label uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/labels/{uuid} [get]
func (co *controller) getMerchLabels(c *gin.Context) {
const logMsg = "Merch | Get merch labels"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
merchUuid := c.Param("uuid")
if merchUuid == "" {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getMerchLabels(userUuid, merchUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Получить нулевые цены
// @Description Получить нулевые цены
// @Tags Merch zero prices
// @Security BearerAuth
// @Produce json
// @Success 200 {array} ZeroPrice
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices [get]
func (co *controller) getZeroPrices(c *gin.Context) {
const logMsg = "Merch | Get zero prices"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
response, err := co.service.getZeroPrices(userUuid)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.JSON(http.StatusOK, response)
}
// @Summary Пометить нулевые цены как удаленные
// @Description Пометить нулевые цены как удаленные
// @Tags Merch zero prices
// @Security BearerAuth
// @Accept json
// @Param payload body DeleteZeroPrices true "payload"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices [delete]
func (co *controller) deleteZeroPrices(c *gin.Context) {
const logMsg = "Merch | Delete zero prices"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
var payload []DeleteZeroPrices
if err = c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteZeroPrices(userUuid, payload); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}
// @Summary Пометить нулевые цены как удаленные за указанный период
// @Description Пометить нулевые цены как удаленные за указанный период
// @Tags Merch zero prices
// @Security BearerAuth
// @Param start query string true "start"
// @Param end query string true "end"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/zeroprices/period [delete]
func (co *controller) deleteZeroPricesPeriod(c *gin.Context) {
const logMsg = "Merch | Delete zero prices period"
userUuid, err := co.utils.GetUserUuidFromContext(c)
if err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
start, err := co.utils.ParseTime(c.Query("start"))
if err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
end, err := co.utils.ParseTime(c.Query("end"))
if err != nil {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
if err = co.service.deleteZeroPricesPeriod(userUuid, start, end); err != nil {
c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()})
log.WithError(err).Error(logMsg)
return
}
c.Status(http.StatusOK)
}

View file

@ -1,5 +1,7 @@
package merch
import "time"
type merchBundle struct {
Merch *Merch
Surugaya *Surugaya
@ -10,6 +12,7 @@ type MerchDTO struct {
Name string `json:"name"`
OriginSurugaya SurugayaDTO `json:"origin_surugaya"`
OriginMandarake MandarakeDTO `json:"origin_mandarake"`
Labels []string `json:"labels,omitempty" gorm:"-"`
}
type SurugayaDTO struct {
@ -29,6 +32,7 @@ type SingleMerchResponse struct {
type ListResponse struct {
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Labels []string `json:"labels,omitempty" gorm:"-"`
}
type PriceEntry struct {
@ -58,3 +62,34 @@ type ImageLink struct {
Link string `json:"link"`
ETag string `json:"etag"`
}
type LabelDTO struct {
Name string `json:"name"`
Color string `json:"color,omitempty"`
BgColor string `json:"bg_color,omitempty"`
}
type LabelsList struct {
LabelUuid string `json:"label_uuid"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
BgColor string `json:"bg_color,omitempty"`
}
type LabelLink struct {
MerchUuid string `json:"merch_uuid"`
LabelUuid string `json:"label_uuid"`
}
type ZeroPrice struct {
Id int `json:"id"`
CreatedAt time.Time `json:"created_at"`
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Origin Origin `json:"origin"`
}
type DeleteZeroPrices struct {
Id uint `json:"id"`
MerchUuid string `json:"merch_uuid"`
}

View file

@ -1,9 +1,9 @@
package merch
import (
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"time"
)
@ -16,7 +16,8 @@ type Handler struct {
type Deps struct {
DB *gorm.DB
Utils interfaces.Utils
Media interfaces.MediaStorage
//Media interfaces.MediaStorage
ImageStorage is.ImageStorageClient
}
func NewHandler(deps Deps) *Handler {
@ -24,18 +25,24 @@ func NewHandler(deps Deps) *Handler {
expires := time.Minute * 5
r := NewRepo(deps.DB)
s := newService(r, deps.Media, packageBucketName, expires)
s := newService(serviceDeps{
repo: r,
//media: deps.Media,
bucketName: packageBucketName,
expires: expires,
imageStorage: deps.ImageStorage,
})
c := newController(s, deps.Utils, expires)
media := deps.Media
log.WithFields(log.Fields{
"addr": media,
}).Debug("Merch handler constructor | Media provider")
exists, err := media.CheckBucketExists(packageBucketName)
if err != nil || !exists {
log.WithError(err).Fatal("Merch handler constructor | Failed to ensure bucket exists")
}
//media := deps.Media
//log.WithFields(log.Fields{
// "addr": media,
//}).Debug("Merch handler constructor | Media provider")
//
//exists, err := media.CheckBucketExists(packageBucketName)
//if err != nil || !exists {
// log.WithError(err).Fatal("Merch handler constructor | Failed to ensure bucket exists")
//}
return &Handler{
repo: r,

View file

@ -1,6 +1,7 @@
package merch
import (
"fmt"
"strconv"
"time"
)
@ -17,3 +18,14 @@ func getPeriod(days string) time.Time {
return time.Now().UTC().Add(-(time.Duration(daysInt) * time.Hour * 24))
}
func (s *service) makeObject(userUuid, merchUuid, imageType string) (string, error) {
switch imageType {
case "thumbnail":
return fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid), nil
case "full":
return fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid), nil
default:
return "", fmt.Errorf("unknown image type %s", imageType)
}
}

View file

@ -44,9 +44,35 @@ func (Mandarake) TableName() string {
type Price struct {
Id uint `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt sql.NullTime `json:"updated_at" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"`
UpdatedAt sql.NullTime `json:"updated_at,omitempty" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at,omitempty" gorm:"column:deleted_at"`
MerchUuid string `json:"merch_uuid" gorm:"column:merch_uuid"`
Price int `json:"price" gorm:"column:price"`
Origin Origin `json:"origin" gorm:"column:origin;type:integer"`
}
type Label struct {
Id uint `json:"-" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"`
LabelUuid string `json:"label_uuid" gorm:"column:label_uuid"`
UserUuid string `json:"user_uuid" gorm:"column:user_uuid"`
Name string `json:"name" gorm:"column:name"`
Color string `json:"color" gorm:"column:color"`
BgColor string `json:"bg_color" gorm:"column:bg_color"`
}
func (Label) TableName() string {
return "labels"
}
type CardLabel struct {
LabelUuid string `json:"label_uuid"`
UserUuid string `json:"user_uuid"`
MerchUuid string `json:"merch_uuid"`
}
func (CardLabel) TableName() string {
return "card_labels"
}

View file

@ -22,6 +22,7 @@ type repository interface {
addMerch(bundle merchBundle) error
merchRecordExists(userUuid, merchUuid string) (bool, error)
userOwnsMerchUuids(userUuid string, merchUuids []string) ([]Merch, error)
getSingleMerch(userUuid, merchUuid string) (merchBundle, error)
getAllMerch(userUuid string) ([]ListResponse, error)
@ -32,11 +33,27 @@ type repository interface {
getAllUserMerch(userUuid string) ([]Merch, error)
prices
labels
}
type prices interface {
getPricesWithDays(userUuid string, period time.Time) ([]Price, error)
getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error)
getZeroPrices(userUuid string) ([]ZeroPrice, error)
deleteZeroPrices(list []DeleteZeroPrices) error
deleteZeroPricesPeriod(userUuid string, start, end time.Time) error
}
type labels interface {
createLabel(label Label) error
getLabels(userUuid string) ([]Label, error)
updateLabel(userUuid, labelUuid string, label map[string]any) error
deleteLabel(userUuid, labelUuid string) error
attachLabel(label CardLabel) error
detachLabel(label CardLabel) error
getAttachedLabelsByList(list []string) ([]CardLabel, error)
getAttachedLabelsByUuid(userUuid, merchUuid string) ([]CardLabel, error)
}
func (r *Repo) addMerch(bundle merchBundle) error {
@ -63,11 +80,28 @@ func (r *Repo) merchRecordExists(userUuid, merchUuid string) (bool, error) {
FROM merch
WHERE user_uuid = ?
AND merch_uuid = ?
AND deleted_at IS NULL
);`, userUuid, merchUuid).Scan(&exists).Error
return exists, err
}
func (r *Repo) userOwnsMerchUuids(userUuid string, merchUuids []string) ([]Merch, error) {
var ownsUuids []Merch
err := r.db.Model(&Merch{}).
Select("merch_uuid").
Where("user_uuid = ?", userUuid).
Where("merch_uuid IN (?)", merchUuids).
Where("deleted_at IS NULL").
Find(&ownsUuids).Error
if err != nil {
return nil, err
}
return ownsUuids, nil
}
func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
var merch Merch
if err := r.db.
@ -238,3 +272,119 @@ func (r *Repo) upsertOrigin(model any) error {
DoUpdates: clause.AssignmentColumns([]string{"link"}),
}).Create(model).Error
}
func (r *Repo) createLabel(label Label) error {
return r.db.Model(&Label{}).Create(&label).Error
}
func (r *Repo) getLabels(userUuid string) ([]Label, error) {
var labelsList []Label
if err := r.db.
Model(&Label{}).
Where("user_uuid = ?", userUuid).
Where("deleted_at IS NULL").
Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) updateLabel(userUuid, labelUuid string, label map[string]any) error {
return r.db.Model(&Label{}).
Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid).
Updates(label).Error
}
func (r *Repo) deleteLabel(userUuid, labelUuid string) error {
return r.db.Model(&Label{}).
Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid).
Update("deleted_at", time.Now().UTC()).Error
}
func (r *Repo) attachLabel(label CardLabel) error {
return r.db.Model(&CardLabel{}).Create(&label).Error
}
func (r *Repo) detachLabel(label CardLabel) error {
return r.db.
Where("user_uuid = ? AND label_uuid = ? AND merch_uuid = ?", label.UserUuid, label.LabelUuid, label.MerchUuid).
Delete(&CardLabel{}).Error
}
func (r *Repo) getAttachedLabelsByList(list []string) ([]CardLabel, error) {
var labelsList []CardLabel
if err := r.db.Model(&CardLabel{}).Where("merch_uuid IN ?", list).Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) getAttachedLabelsByUuid(userUuid, merchUuid string) ([]CardLabel, error) {
var labelsList []CardLabel
if err := r.db.Model(&CardLabel{}).Where("user_uuid = ? AND merch_uuid = ?", userUuid, merchUuid).Find(&labelsList).Error; err != nil {
return nil, err
}
return labelsList, nil
}
func (r *Repo) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
var priceList []ZeroPrice
if err := r.db.Raw(`
WITH price_with_neighbors AS (
SELECT
p.id, p.created_at, p.merch_uuid, p.price, p.origin, m.name,
LAG(price) OVER (PARTITION BY p.merch_uuid, p.origin ORDER BY p.created_at, p.id) AS prev_price,
LEAD(price) OVER (PARTITION BY p.merch_uuid, p.origin ORDER BY p.created_at, p.id) AS next_price
FROM prices AS p
JOIN merch as m ON m.merch_uuid = p.merch_uuid
WHERE p.deleted_at IS NULL
AND m.deleted_at IS NULL
AND m.user_uuid = ?)
SELECT
id, created_at, merch_uuid, origin, name
FROM price_with_neighbors
WHERE
price = 0
AND prev_price IS NOT NULL
AND prev_price > 0
AND next_price IS NOT NULL
AND next_price > 0;
`, userUuid).Scan(&priceList).Error; err != nil {
return nil, err
}
return priceList, nil
}
func (r *Repo) deleteZeroPrices(list []DeleteZeroPrices) error {
for _, item := range list {
if err := r.db.Model(&Price{}).
Where("id = ? AND merch_uuid = ?", item.Id, item.MerchUuid).
Update("deleted_at", time.Now().UTC()).Error; err != nil {
return err
}
}
return nil
}
func (r *Repo) deleteZeroPricesPeriod(userUuid string, start, end time.Time) error {
if err := r.db.Exec(`
UPDATE prices
SET deleted_at = ?
FROM merch
WHERE prices.merch_uuid = merch.merch_uuid
AND merch.user_uuid = ?
AND prices.price = 0
AND prices.deleted_at IS NULL
AND prices.created_at BETWEEN ? AND ?;
`, time.Now().UTC(), userUuid, start, end).Error; err != nil {
return err
}
return nil
}

View file

@ -13,6 +13,7 @@ import (
"image/jpeg"
"io"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"mime/multipart"
"path/filepath"
"strings"
@ -24,14 +25,24 @@ type service struct {
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(repo repository, media interfaces.MediaStorage, bucketName string, expires time.Duration) *service {
type serviceDeps struct {
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(deps serviceDeps) *service {
return &service{
repo: repo,
media: media,
bucketName: bucketName,
expires: expires,
repo: deps.repo,
media: deps.media,
bucketName: deps.bucketName,
expires: deps.expires,
imageStorage: deps.imageStorage,
}
}
@ -90,7 +101,34 @@ func (s *service) getSingleMerch(userUuid, merchUuid string) (MerchDTO, error) {
}
func (s *service) getAllMerch(userUuid string) ([]ListResponse, error) {
return s.repo.getAllMerch(userUuid)
const logMsg = "Merch service | Get all merch"
allMerch, err := s.repo.getAllMerch(userUuid)
if err != nil {
return nil, err
}
ids := make([]string, 0, len(allMerch))
for _, m := range allMerch {
ids = append(ids, m.MerchUuid)
}
cardLabels, err := s.repo.getAttachedLabelsByList(ids)
if err != nil {
return nil, err
}
log.WithField("content", cardLabels).Debug(logMsg)
clMap := make(map[string][]string)
for _, cl := range cardLabels {
clMap[cl.MerchUuid] = append(clMap[cl.MerchUuid], cl.LabelUuid)
}
for item := range allMerch {
allMerch[item].Labels = clMap[allMerch[item].MerchUuid]
}
return allMerch, nil
}
func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
@ -210,6 +248,9 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
}, nil
}
// uploadMerchImage
// Deprecated.
// Use only with MinIO storage. Use mtUploadMerchImage for merch-tracker images storage.
func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, imageType string, file *multipart.FileHeader) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
@ -303,7 +344,30 @@ func (s *service) uploadMerchImage(ctx context.Context, userUuid, merchUuid, ima
return nil
}
func (s *service) getMerchImage(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
// getPublicImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPublicImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil {
return ImageLink{}, err
}
link, etag, err := s.media.GetPublicLink(ctx, s.bucketName, object)
if err != nil {
return ImageLink{}, err
}
return ImageLink{
Link: link,
ETag: etag,
}, nil
}
// getPresignedImageLink
// Deprecated.
// Use only with MinIO storage.
func (s *service) getPresignedImageLink(ctx context.Context, userUuid, merchUuid, imageType string) (ImageLink, error) {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return ImageLink{}, err
@ -313,17 +377,12 @@ func (s *service) getMerchImage(ctx context.Context, userUuid, merchUuid, imageT
return ImageLink{}, fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
var object string
switch imageType {
case "thumbnail":
object = fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)
case "full":
object = fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)
default:
return ImageLink{}, fmt.Errorf("unknown image type %s", imageType)
object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil {
return ImageLink{}, err
}
link, err := s.media.Get(ctx, s.bucketName, object, s.expires, nil)
link, err := s.media.GetPresignedLink(ctx, s.bucketName, object, s.expires, nil)
if err != nil {
return ImageLink{}, err
}
@ -339,6 +398,9 @@ func (s *service) getMerchImage(ctx context.Context, userUuid, merchUuid, imageT
}, nil
}
// deleteMerchImage
// Deprecated.
// Use only with MinIO storage.
func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
@ -349,13 +411,14 @@ func (s *service) deleteMerchImage(ctx context.Context, userUuid, merchUuid stri
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)); err != nil {
return err
}
if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)); err != nil {
return err
}
//uncomment for MinIO
//if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid)); err != nil {
// return err
//}
//
//if err = s.media.Delete(ctx, s.bucketName, fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid)); err != nil {
// return err
//}
return nil
}
@ -386,3 +449,220 @@ func (s *service) _uploadToStorage(params uploadImageParams) error {
return nil
}
// mtUploadMerchImage
// Upload new/rewrite existing image to merch-tracker images storage
func (s *service) mtUploadMerchImage(ctx context.Context, userUuid, merchUuid string, file *multipart.FileHeader) (*is.UploadMerchImageResponse, error) {
const uploadMerchImage = "Merch service | Upload merch image"
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
if !exists {
err = fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
f, err := file.Open()
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
response, err := s.imageStorage.UploadImage(ctx, &is.UploadMerchImageRequest{
ImageData: data,
UserUuid: userUuid,
MerchUuid: merchUuid,
})
if err != nil {
log.WithError(err).Error(uploadMerchImage)
return nil, err
}
return response, nil
}
// mtDeleteMerchImage
// Delete all merch images for given user and merch uuid-s from merch-tracker images storage
func (s *service) mtDeleteMerchImage(ctx context.Context, userUuid, merchUuid string) error {
exists, err := s.repo.merchRecordExists(userUuid, merchUuid)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
s.imageStorage.DeleteImage(ctx, &is.DeleteImageRequest{
UserUuid: userUuid,
MerchUuid: merchUuid,
})
return nil
}
func (s *service) createLabel(label LabelDTO, userUuid string) error {
now := time.Now().UTC()
if label.Name == "" {
return fmt.Errorf("label name is required")
}
newLabel := Label{
CreatedAt: now,
UpdatedAt: now,
DeletedAt: sql.NullTime{Time: time.Time{}, Valid: false},
LabelUuid: uuid.NewString(),
UserUuid: userUuid,
Name: label.Name,
Color: label.Color,
BgColor: label.BgColor,
}
return s.repo.createLabel(newLabel)
}
func (s *service) getLabels(userUuid string) ([]LabelsList, error) {
stored, err := s.repo.getLabels(userUuid)
if err != nil {
return nil, err
}
response := make([]LabelsList, 0, len(stored))
for _, label := range stored {
response = append(response, LabelsList{
LabelUuid: label.LabelUuid,
Name: label.Name,
Color: label.Color,
BgColor: label.BgColor,
})
}
return response, nil
}
func (s *service) updateLabel(userUuid, labelUuid string, label LabelDTO) error {
updateMap := make(map[string]any, 3)
if label.Name != "" {
updateMap["name"] = label.Name
}
if label.Color != "" {
updateMap["color"] = label.Color
}
if label.BgColor != "" {
updateMap["bg_color"] = label.BgColor
}
return s.repo.updateLabel(userUuid, labelUuid, updateMap)
}
func (s *service) deleteLabel(userUuid, labelUuid string) error {
return s.repo.deleteLabel(userUuid, labelUuid)
}
func (s *service) attachLabel(userUuid string, label LabelLink) error {
if label.LabelUuid == "" || label.MerchUuid == "" {
return fmt.Errorf("both label and merch uuid-s are required")
}
attach := CardLabel{
LabelUuid: label.LabelUuid,
UserUuid: userUuid,
MerchUuid: label.MerchUuid,
}
return s.repo.attachLabel(attach)
}
func (s *service) detachLabel(userUuid string, label LabelLink) error {
if label.LabelUuid == "" || label.MerchUuid == "" {
return fmt.Errorf("both label and merch uuid-s are required")
}
detach := CardLabel{
LabelUuid: label.LabelUuid,
UserUuid: userUuid,
MerchUuid: label.MerchUuid,
}
return s.repo.detachLabel(detach)
}
func (s *service) getMerchLabels(userUuid, merchUuid string) ([]string, error) {
getLabels, err := s.repo.getAttachedLabelsByUuid(userUuid, merchUuid)
if err != nil {
return nil, err
}
response := make([]string, 0, len(getLabels))
for _, label := range getLabels {
response = append(response, label.LabelUuid)
}
return response, nil
}
func (s *service) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
return s.repo.getZeroPrices(userUuid)
}
func (s *service) deleteZeroPrices(userUuid string, list []DeleteZeroPrices) error {
const delMsg = "Merch - service | Delete zero prices"
if len(list) == 0 {
return nil
}
ids := make([]string, 0, len(list))
for _, item := range list {
ids = append(ids, item.MerchUuid)
}
uniqueMap := make(map[string]struct{}, len(list))
uniqueIds := make([]string, 0, len(list))
for _, id := range ids {
if _, ok := uniqueMap[id]; !ok {
uniqueMap[id] = struct{}{}
uniqueIds = append(uniqueIds, id)
}
}
log.WithField("uuid count", len(uniqueIds)).Debug(delMsg)
owns, err := s.repo.userOwnsMerchUuids(userUuid, uniqueIds)
if err != nil {
return err
}
if len(owns) < 1 {
return errors.New("wrong ids")
}
ownsMap := make(map[string]struct{}, len(owns))
for _, own := range owns {
ownsMap[own.MerchUuid] = struct{}{}
}
toDelete := make([]DeleteZeroPrices, 0, len(owns))
for _, item := range list {
if _, ok := ownsMap[item.MerchUuid]; ok {
toDelete = append(toDelete, item)
}
}
return s.repo.deleteZeroPrices(toDelete)
}
func (s *service) deleteZeroPricesPeriod(userUuid string, start, end time.Time) error {
return s.repo.deleteZeroPricesPeriod(userUuid, start, end)
}

View file

@ -26,10 +26,10 @@ func newController(service *service, utils interfaces.Utils) *controller {
func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, refreshMW gin.HandlerFunc) {
userGroup := r.Group("/user")
userGroup.POST("/", h.controller.register)
userGroup.GET("/", authMW, h.controller.get)
userGroup.PUT("/", authMW, h.controller.update)
userGroup.DELETE("/", authMW, h.controller.delete)
userGroup.POST("", h.controller.register)
userGroup.GET("", authMW, h.controller.get)
userGroup.PUT("", authMW, h.controller.update)
userGroup.DELETE("", authMW, h.controller.delete)
//auth
h.controller.authPath = fmt.Sprintf("%s/user/auth", h.apiPrefix)

View file

@ -40,7 +40,7 @@ func (r *repo) getByUuid(userUuid string) (user User, err error) {
}
func (r *repo) update(user map[string]any) error {
return r.db.Where("uuid = ?", user["uuid"]).Updates(&user).Error
return r.db.Model(&User{}).Where("uuid = ?", user["uuid"]).Updates(&user).Error
}
func (r *repo) delete(userUuid string) error {

View file

@ -1,106 +1,18 @@
package grpcService
import (
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/shared"
pb "merch-parser-api/proto/taskProcessor"
"time"
)
type repoServer struct {
pb.UnimplementedTaskProcessorServer
taskProvider interfaces.TaskProvider
}
func NewGrpcServer(taskProvider interfaces.TaskProvider) *grpc.Server {
srv := grpc.NewServer()
repoSrv := &repoServer{
taskProvider: taskProvider,
}
pb.RegisterTaskProcessorServer(srv, repoSrv)
return srv
}
func (r *repoServer) RequestTask(_ *emptypb.Empty, stream pb.TaskProcessor_RequestTaskServer) error {
tasks, err := r.taskProvider.PrepareTasks()
if err != nil {
log.WithField("err", err).Error("gRPC Server | Request task error")
return err
}
for _, task := range tasks {
if err = stream.Send(&pb.Task{
MerchUuid: task.MerchUuid,
OriginSurugayaLink: task.OriginSurugayaLink,
OriginMandarakeLink: task.OriginMandarakeLink,
}); err != nil {
log.WithField("err", err).Error("gRPC Server | Stream send error")
return err
}
}
return nil
}
func (r *repoServer) SendResult(stream pb.TaskProcessor_SendResultServer) error {
saveInterval := time.Second * 2
batch := make([]shared.TaskResult, 0)
ticker := time.NewTicker(saveInterval)
defer ticker.Stop()
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Batch insert")
}
}
}
}
}()
for {
response, err := stream.Recv()
if err == io.EOF {
log.Debug("gRPC EOF")
break
}
if err != nil {
log.WithField("err", err).Error("gRPC Server | Receive")
return err
}
entry := shared.TaskResult{
MerchUuid: response.MerchUuid,
Origin: response.OriginName,
Price: response.Price,
}
batch = append(batch, entry)
log.WithField("response", entry).Debug("gRPC Server | Receive success")
}
close(done)
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Last data batch insert")
return err
}
}
return nil
}

View file

@ -0,0 +1,95 @@
package grpcService
import (
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/types/known/emptypb"
"io"
"merch-parser-api/internal/interfaces"
"merch-parser-api/internal/shared"
pb "merch-parser-api/proto/taskProcessor"
"time"
)
type repoServer struct {
pb.UnimplementedTaskProcessorServer
taskProvider interfaces.TaskProvider
}
func (r *repoServer) RequestTask(_ *emptypb.Empty, stream pb.TaskProcessor_RequestTaskServer) error {
tasks, err := r.taskProvider.PrepareTasks()
if err != nil {
log.WithField("err", err).Error("gRPC Server | Request task error")
return err
}
for _, task := range tasks {
if err = stream.Send(&pb.Task{
MerchUuid: task.MerchUuid,
OriginSurugayaLink: task.OriginSurugayaLink,
OriginMandarakeLink: task.OriginMandarakeLink,
}); err != nil {
log.WithField("err", err).Error("gRPC Server | Stream send error")
return err
}
}
return nil
}
func (r *repoServer) SendResult(stream pb.TaskProcessor_SendResultServer) error {
saveInterval := time.Second * 2
batch := make([]shared.TaskResult, 0)
ticker := time.NewTicker(saveInterval)
defer ticker.Stop()
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
case <-ticker.C:
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Batch insert")
}
}
}
}
}()
for {
response, err := stream.Recv()
if err == io.EOF {
log.Debug("gRPC EOF")
break
}
if err != nil {
log.WithField("err", err).Error("gRPC Server | Receive")
return err
}
entry := shared.TaskResult{
MerchUuid: response.MerchUuid,
Origin: response.OriginName,
Price: response.Price,
}
batch = append(batch, entry)
log.WithField("response", entry).Debug("gRPC Server | Receive success")
}
close(done)
if len(batch) > 0 {
err := r.taskProvider.InsertPrices(batch)
if err != nil {
log.WithField("err", err).Error("gRPC Server | Last data batch insert")
return err
}
}
return nil
}

View file

@ -0,0 +1,27 @@
package imagesProvider
import (
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
is "merch-parser-api/proto/imageStorage"
)
type Handler struct{}
func NewClient(address string) is.ImageStorageClient {
var opts []grpc.DialOption
insec := grpc.WithTransportCredentials(insecure.NewCredentials())
opts = append(opts, insec)
conn, err := grpc.NewClient(address, opts...)
if err != nil {
log.Fatal(err)
}
log.WithFields(log.Fields{
"address": address,
}).Debug("gRPC | API client")
return is.NewImageStorageClient(conn)
}

View file

@ -7,10 +7,13 @@ import (
"time"
)
// MinIO service replaced by imagesProvider
type MediaStorage interface {
CheckBucketExists(bucketName string) (bool, error)
Upload(ctx context.Context, bucket, object string, reader io.Reader, size int64) error
Get(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error)
GetPublicLink(ctx context.Context, bucket, object string) (string, string, error)
GetPresignedLink(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error)
Delete(ctx context.Context, bucket, object string) error
GetObjectEtag(ctx context.Context, bucketName, object string) (string, error)
}

View file

@ -1,6 +1,9 @@
package interfaces
import "github.com/gin-gonic/gin"
import (
"github.com/gin-gonic/gin"
"time"
)
type Utils interface {
IsEmail(email string) bool
@ -8,4 +11,5 @@ type Utils interface {
GetRefreshUuidFromContext(c *gin.Context) (string, error)
HashPassword(password string) (string, error)
ComparePasswords(hashedPassword string, plainPassword string) error
ParseTime(t string) (time.Time, error)
}

View file

@ -14,7 +14,6 @@ type Deps struct {
Endpoint string
User string
Password string
Domain string
Secure string
}
@ -38,6 +37,6 @@ func NewHandler(deps Deps) *Handler {
}).Debug("Media storage | Created minio client")
return &Handler{
newService(minioClient),
newService(minioClient, deps.Endpoint, secureMode),
}
}

View file

@ -2,22 +2,26 @@ package mediaStorage
import (
"context"
"fmt"
"github.com/minio/minio-go/v7"
log "github.com/sirupsen/logrus"
"io"
"net/url"
"strings"
"time"
)
type Service struct {
client *minio.Client
domain string
endpoint string
secureMode bool
}
func newService(client *minio.Client) *Service {
func newService(client *minio.Client, endpoint string, secureMode bool) *Service {
return &Service{
client: client,
endpoint: endpoint,
secureMode: secureMode,
}
}
@ -37,7 +41,33 @@ func (s *Service) Upload(ctx context.Context, bucket, object string, reader io.R
return err
}
func (s *Service) Get(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error) {
func (s *Service) GetPublicLink(ctx context.Context, bucket, object string) (string, string, error) {
stat, err := s.client.StatObject(ctx, bucket, object, minio.StatObjectOptions{})
if err != nil {
if err.Error() == minio.ToErrorResponse(err).Error() {
return "", "", nil
}
log.WithFields(log.Fields{
"error": err,
"key": bucket + "/" + object,
}).Error("Media storage | Failed to get public link")
return "", "", err
}
var scheme string
if s.secureMode {
scheme = "https"
} else {
scheme = "http"
}
link := fmt.Sprintf("%s://%s/%s/%s", scheme, strings.TrimRight(s.endpoint, "/"), bucket, object)
log.WithFields(log.Fields{"link": link}).Debug("Media storage | Get public link")
return link, stat.ETag, nil
}
func (s *Service) GetPresignedLink(ctx context.Context, bucket, object string, expires time.Duration, params url.Values) (string, error) {
presigned, err := s.client.PresignedGetObject(ctx, bucket, object, expires, params)
if err != nil {
return "", err

View file

@ -56,3 +56,26 @@ CREATE TABLE prices(
price INT NULL,
origin INT
);
CREATE TABLE labels(
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NULL,
deleted_at TIMESTAMP WITH TIME ZONE NULL,
user_uuid VARCHAR(36) NOT NULL,
label_uuid VARCHAR(36) NOT NULL,
name VARCHAR(255),
color VARCHAR(32),
bg_color VARCHAR(32)
);
CREATE TABLE card_labels (
id BIGSERIAL PRIMARY KEY,
user_uuid VARCHAR(36) NOT NULL,
label_uuid VARCHAR(36) NOT NULL,
merch_uuid VARCHAR(36) NOT NULL
);
ALTER TABLE card_labels
ADD CONSTRAINT card_labels_unique_user_label_merch
UNIQUE (user_uuid, label_uuid, merch_uuid);

11
pkg/utils/time.go Normal file
View file

@ -0,0 +1,11 @@
package utils
import "time"
func (u *Utils) ParseTime(t string) (time.Time, error) {
timeStr, err := time.Parse(time.RFC3339, t)
if err != nil {
return time.Time{}, err
}
return timeStr, nil
}

27
proto/imageStorage.proto Normal file
View file

@ -0,0 +1,27 @@
syntax="proto3";
import "google/protobuf/empty.proto";
package imageStorage;
option go_package = "imageStorage/pkg/proto/imageStorage";
message UploadMerchImageRequest{
bytes imageData = 1;
string userUuid = 2;
string merchUuid = 3;
}
message UploadMerchImageResponse {
string fullImage = 1;
string thumbnail = 2;
}
message DeleteImageRequest {
string userUuid = 1;
string merchUuid = 2;
}
service ImageStorage {
rpc UploadImage(UploadMerchImageRequest) returns (UploadMerchImageResponse);
rpc DeleteImage(DeleteImageRequest) returns (google.protobuf.Empty);
}

View file

@ -0,0 +1,261 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.8
// protoc v6.32.1
// source: imageStorage.proto
package imageStorage
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type UploadMerchImageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ImageData []byte `protobuf:"bytes,1,opt,name=imageData,proto3" json:"imageData,omitempty"`
UserUuid string `protobuf:"bytes,2,opt,name=userUuid,proto3" json:"userUuid,omitempty"`
MerchUuid string `protobuf:"bytes,3,opt,name=merchUuid,proto3" json:"merchUuid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UploadMerchImageRequest) Reset() {
*x = UploadMerchImageRequest{}
mi := &file_imageStorage_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UploadMerchImageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UploadMerchImageRequest) ProtoMessage() {}
func (x *UploadMerchImageRequest) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UploadMerchImageRequest.ProtoReflect.Descriptor instead.
func (*UploadMerchImageRequest) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{0}
}
func (x *UploadMerchImageRequest) GetImageData() []byte {
if x != nil {
return x.ImageData
}
return nil
}
func (x *UploadMerchImageRequest) GetUserUuid() string {
if x != nil {
return x.UserUuid
}
return ""
}
func (x *UploadMerchImageRequest) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
type UploadMerchImageResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
FullImage string `protobuf:"bytes,1,opt,name=fullImage,proto3" json:"fullImage,omitempty"`
Thumbnail string `protobuf:"bytes,2,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UploadMerchImageResponse) Reset() {
*x = UploadMerchImageResponse{}
mi := &file_imageStorage_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UploadMerchImageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UploadMerchImageResponse) ProtoMessage() {}
func (x *UploadMerchImageResponse) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UploadMerchImageResponse.ProtoReflect.Descriptor instead.
func (*UploadMerchImageResponse) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{1}
}
func (x *UploadMerchImageResponse) GetFullImage() string {
if x != nil {
return x.FullImage
}
return ""
}
func (x *UploadMerchImageResponse) GetThumbnail() string {
if x != nil {
return x.Thumbnail
}
return ""
}
type DeleteImageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserUuid string `protobuf:"bytes,1,opt,name=userUuid,proto3" json:"userUuid,omitempty"`
MerchUuid string `protobuf:"bytes,2,opt,name=merchUuid,proto3" json:"merchUuid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteImageRequest) Reset() {
*x = DeleteImageRequest{}
mi := &file_imageStorage_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteImageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteImageRequest) ProtoMessage() {}
func (x *DeleteImageRequest) ProtoReflect() protoreflect.Message {
mi := &file_imageStorage_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteImageRequest.ProtoReflect.Descriptor instead.
func (*DeleteImageRequest) Descriptor() ([]byte, []int) {
return file_imageStorage_proto_rawDescGZIP(), []int{2}
}
func (x *DeleteImageRequest) GetUserUuid() string {
if x != nil {
return x.UserUuid
}
return ""
}
func (x *DeleteImageRequest) GetMerchUuid() string {
if x != nil {
return x.MerchUuid
}
return ""
}
var File_imageStorage_proto protoreflect.FileDescriptor
const file_imageStorage_proto_rawDesc = "" +
"\n" +
"\x12imageStorage.proto\x12\fimageStorage\x1a\x1bgoogle/protobuf/empty.proto\"q\n" +
"\x17UploadMerchImageRequest\x12\x1c\n" +
"\timageData\x18\x01 \x01(\fR\timageData\x12\x1a\n" +
"\buserUuid\x18\x02 \x01(\tR\buserUuid\x12\x1c\n" +
"\tmerchUuid\x18\x03 \x01(\tR\tmerchUuid\"V\n" +
"\x18UploadMerchImageResponse\x12\x1c\n" +
"\tfullImage\x18\x01 \x01(\tR\tfullImage\x12\x1c\n" +
"\tthumbnail\x18\x02 \x01(\tR\tthumbnail\"N\n" +
"\x12DeleteImageRequest\x12\x1a\n" +
"\buserUuid\x18\x01 \x01(\tR\buserUuid\x12\x1c\n" +
"\tmerchUuid\x18\x02 \x01(\tR\tmerchUuid2\xb5\x01\n" +
"\fImageStorage\x12\\\n" +
"\vUploadImage\x12%.imageStorage.UploadMerchImageRequest\x1a&.imageStorage.UploadMerchImageResponse\x12G\n" +
"\vDeleteImage\x12 .imageStorage.DeleteImageRequest\x1a\x16.google.protobuf.EmptyB%Z#imageStorage/pkg/proto/imageStorageb\x06proto3"
var (
file_imageStorage_proto_rawDescOnce sync.Once
file_imageStorage_proto_rawDescData []byte
)
func file_imageStorage_proto_rawDescGZIP() []byte {
file_imageStorage_proto_rawDescOnce.Do(func() {
file_imageStorage_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_imageStorage_proto_rawDesc), len(file_imageStorage_proto_rawDesc)))
})
return file_imageStorage_proto_rawDescData
}
var file_imageStorage_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_imageStorage_proto_goTypes = []any{
(*UploadMerchImageRequest)(nil), // 0: imageStorage.UploadMerchImageRequest
(*UploadMerchImageResponse)(nil), // 1: imageStorage.UploadMerchImageResponse
(*DeleteImageRequest)(nil), // 2: imageStorage.DeleteImageRequest
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
}
var file_imageStorage_proto_depIdxs = []int32{
0, // 0: imageStorage.ImageStorage.UploadImage:input_type -> imageStorage.UploadMerchImageRequest
2, // 1: imageStorage.ImageStorage.DeleteImage:input_type -> imageStorage.DeleteImageRequest
1, // 2: imageStorage.ImageStorage.UploadImage:output_type -> imageStorage.UploadMerchImageResponse
3, // 3: imageStorage.ImageStorage.DeleteImage:output_type -> google.protobuf.Empty
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_imageStorage_proto_init() }
func file_imageStorage_proto_init() {
if File_imageStorage_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_imageStorage_proto_rawDesc), len(file_imageStorage_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_imageStorage_proto_goTypes,
DependencyIndexes: file_imageStorage_proto_depIdxs,
MessageInfos: file_imageStorage_proto_msgTypes,
}.Build()
File_imageStorage_proto = out.File
file_imageStorage_proto_goTypes = nil
file_imageStorage_proto_depIdxs = nil
}

View file

@ -0,0 +1,160 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.32.1
// source: imageStorage.proto
package imageStorage
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
ImageStorage_UploadImage_FullMethodName = "/imageStorage.ImageStorage/UploadImage"
ImageStorage_DeleteImage_FullMethodName = "/imageStorage.ImageStorage/DeleteImage"
)
// ImageStorageClient is the client API for ImageStorage service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ImageStorageClient interface {
UploadImage(ctx context.Context, in *UploadMerchImageRequest, opts ...grpc.CallOption) (*UploadMerchImageResponse, error)
DeleteImage(ctx context.Context, in *DeleteImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type imageStorageClient struct {
cc grpc.ClientConnInterface
}
func NewImageStorageClient(cc grpc.ClientConnInterface) ImageStorageClient {
return &imageStorageClient{cc}
}
func (c *imageStorageClient) UploadImage(ctx context.Context, in *UploadMerchImageRequest, opts ...grpc.CallOption) (*UploadMerchImageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UploadMerchImageResponse)
err := c.cc.Invoke(ctx, ImageStorage_UploadImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *imageStorageClient) DeleteImage(ctx context.Context, in *DeleteImageRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, ImageStorage_DeleteImage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ImageStorageServer is the server API for ImageStorage service.
// All implementations must embed UnimplementedImageStorageServer
// for forward compatibility.
type ImageStorageServer interface {
UploadImage(context.Context, *UploadMerchImageRequest) (*UploadMerchImageResponse, error)
DeleteImage(context.Context, *DeleteImageRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedImageStorageServer()
}
// UnimplementedImageStorageServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedImageStorageServer struct{}
func (UnimplementedImageStorageServer) UploadImage(context.Context, *UploadMerchImageRequest) (*UploadMerchImageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UploadImage not implemented")
}
func (UnimplementedImageStorageServer) DeleteImage(context.Context, *DeleteImageRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteImage not implemented")
}
func (UnimplementedImageStorageServer) mustEmbedUnimplementedImageStorageServer() {}
func (UnimplementedImageStorageServer) testEmbeddedByValue() {}
// UnsafeImageStorageServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ImageStorageServer will
// result in compilation errors.
type UnsafeImageStorageServer interface {
mustEmbedUnimplementedImageStorageServer()
}
func RegisterImageStorageServer(s grpc.ServiceRegistrar, srv ImageStorageServer) {
// If the following call pancis, it indicates UnimplementedImageStorageServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&ImageStorage_ServiceDesc, srv)
}
func _ImageStorage_UploadImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UploadMerchImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageStorageServer).UploadImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageStorage_UploadImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageStorageServer).UploadImage(ctx, req.(*UploadMerchImageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ImageStorage_DeleteImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteImageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ImageStorageServer).DeleteImage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ImageStorage_DeleteImage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ImageStorageServer).DeleteImage(ctx, req.(*DeleteImageRequest))
}
return interceptor(ctx, in, info, handler)
}
// ImageStorage_ServiceDesc is the grpc.ServiceDesc for ImageStorage service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ImageStorage_ServiceDesc = grpc.ServiceDesc{
ServiceName: "imageStorage.ImageStorage",
HandlerType: (*ImageStorageServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "UploadImage",
Handler: _ImageStorage_UploadImage_Handler,
},
{
MethodName: "DeleteImage",
Handler: _ImageStorage_DeleteImage_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "imageStorage.proto",
}