Compare commits

...

53 commits
v0.1.6 ... main

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
nquidox
947220b65c endpoint env refactor
All checks were successful
/ Make image (push) Successful in 1m22s
2025-10-18 16:38:21 +03:00
nquidox
f3d123ee3b replace domain for links
All checks were successful
/ Make image (push) Successful in 1m35s
2025-10-18 16:07:56 +03:00
nquidox
bb305eab9e replace domain for links
All checks were successful
/ Make image (push) Successful in 1m21s
2025-10-18 15:32:18 +03:00
nquidox
0348dda5cd switch back to alpine
All checks were successful
/ Make image (push) Successful in 1m31s
2025-10-18 14:57:43 +03:00
nquidox
f561869b08 swagger docs update
All checks were successful
/ Make image (push) Successful in 1m29s
2025-10-18 14:11:58 +03:00
nquidox
c2304f6a7d update 2025-10-17 23:48:05 +03:00
nquidox
bc6621154b return etag 2025-10-17 23:47:48 +03:00
nquidox
d1542b274e switch to ubuntu 2025-10-17 23:46:34 +03:00
nquidox
2feda33e26 dev
All checks were successful
/ Make image (push) Successful in 1m39s
2025-10-16 21:00:23 +03:00
nquidox
f6bb647591 update
All checks were successful
/ Make image (push) Successful in 2m0s
2025-10-16 20:55:24 +03:00
nquidox
2d2afffcaf create client log + secure mode env
All checks were successful
/ Make image (push) Successful in 1m21s
2025-10-16 15:41:52 +03:00
nquidox
95b75d0067 change from creation to exists check
All checks were successful
/ Make image (push) Successful in 1m14s
2025-10-15 21:34:32 +03:00
nquidox
38193e8943 conf fix
All checks were successful
/ Make image (push) Successful in 1m14s
2025-10-15 20:48:05 +03:00
nquidox
1fc273c62f update 2025-10-15 20:47:15 +03:00
nquidox
a29d5a8766 update
Some checks failed
/ Make image (push) Failing after 1m4s
2025-10-15 20:28:15 +03:00
nquidox
b469313701 update
Some checks failed
/ Make image (push) Failing after 1m6s
2025-10-15 20:18:19 +03:00
nquidox
d23529e089 swagger docs update
Some checks failed
/ Make image (push) Has been cancelled
2025-10-15 19:46:22 +03:00
nquidox
dec89435a3 merch images crud 2025-10-15 19:46:10 +03:00
nquidox
e708c92d18 media storage config + env 2025-10-15 19:45:49 +03:00
nquidox
262d02e915 update 2025-10-15 19:44:52 +03:00
nquidox
1a67c02e00 created media storage package + interface 2025-10-15 19:44:41 +03:00
nquidox
218e7d652f update
All checks were successful
/ Make image (push) Successful in 1m6s
2025-10-12 14:52:19 +03:00
nquidox
a75d6240b3 action update 2025-10-10 21:58:50 +03:00
31 changed files with 4136 additions and 209 deletions

View file

@ -11,13 +11,21 @@ jobs:
build-and-push:
name: Make image
runs-on: ubuntu-latest
container:
image: ubuntu-latest
options: --privileged -v /var/run/docker.sock:/var/run/docker.sock
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Forgejo
uses: docker/login-action@v3
with:
registry: repo.nqws.ru
username: ${{ secrets.MAINTAINER_USERNAME }}
password: ${{ secrets.MAINTAINER_TOKEN }}
- name: Extract version from tag
id: extract_version
run: |
@ -27,6 +35,6 @@ jobs:
- name: Make image
run: |
docker buildx build --platform linux/amd64 \
--tag repo.nqws.ru/merch-tracker/mtv2-repo-app:latest \
--tag repo.nqws.ru/merch-tracker/mtv2-repo-app:${{ env.VERSION }} \
--tag repo.nqws.ru/${{ github.repository }}:latest \
--tag repo.nqws.ru/${{ github.repository }}:${{ env.VERSION }} \
--push .

View file

@ -1,34 +1,21 @@
FROM golang:1.25.1-alpine3.22 AS builder
RUN apk add --no-cache \
bash \
curl \
git \
ca-certificates
RUN apk add --no-cache tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o main "./cmd"
FROM alpine:3.22
RUN apk add --no-cache \
bash \
curl \
ca-certificates \
tzdata
tzdata \
ca-certificates
COPY --from=builder /app/main /usr/local/bin/app
RUN chmod +x /usr/local/bin/app
RUN adduser -D -s /bin/bash appuser
USER appuser
ENTRYPOINT ["app"]

View file

@ -8,6 +8,14 @@ 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=
MEDIA_STORAGE_SECURE=false
DB_HOST=
DB_PORT=
DB_USER=

View file

@ -9,7 +9,9 @@ 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"
"merch-parser-api/internal/provider/token"
"merch-parser-api/internal/router"
@ -27,8 +29,7 @@ import (
func main() {
log.Debug("Starting merch-parser-api")
//setup config
//c := config.NewConfig()
c := config.DevConfig()
c := config.NewConfig()
ctx := context.Background()
//log level
@ -51,6 +52,19 @@ func main() {
utilsProvider := utils.NewUtils()
log.Debug("Utils provider initialized")
mediaProvider := mediaStorage.NewHandler(mediaStorage.Deps{
Endpoint: c.MediaConf.Endpoint,
User: c.MediaConf.User,
Password: c.MediaConf.Password,
Secure: c.MediaConf.Secure,
})
log.WithFields(log.Fields{
"endpoint": c.MediaConf.Endpoint,
"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,
@ -82,6 +96,8 @@ func main() {
merchModule := merch.NewHandler(merch.Deps{
DB: database,
Utils: utilsProvider,
//Media: mediaProvider,
ImageStorage: imageProvider,
})
//collect modules

View file

@ -3,10 +3,12 @@ package config
import "strings"
type Config struct {
AppConf AppConfig
DBConf DatabaseConfig
JWTConf JWTConfig
GrpcConf GrpcConfig
AppConf AppConfig
DBConf DatabaseConfig
JWTConf JWTConfig
GrpcConf GrpcConfig
MediaConf MediaConfig
ImageConf ImageStorageConfig
}
type AppConfig struct {
@ -40,6 +42,18 @@ type GrpcConfig struct {
GrpcClientPort string
}
type MediaConfig struct {
Endpoint string
User string
Password string
Secure string
}
type ImageStorageConfig struct {
Host string
Port string
}
func NewConfig() *Config {
return &Config{
AppConf: AppConfig{
@ -72,5 +86,17 @@ func NewConfig() *Config {
GrpcServerPort: getEnv("GRPC_SERVER_PORT", ""),
GrpcClientPort: getEnv("GRPC_CLIENT_PORT", ""),
},
MediaConf: MediaConfig{
Endpoint: getEnv("MEDIA_STORAGE_ENDPOINT", ""),
User: getEnv("MEDIA_STORAGE_USER", ""),
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"
],
@ -95,20 +98,168 @@ const docTemplate = `{
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Обновить информацию про мерч",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.UpdateMerchDTO"
}
}
],
"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}": {
"/merch/images/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"tags": [
"Merch"
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"summary": "Получить всю информацию про мерч",
"tags": [
"Merch images"
],
"summary": "Получить картинки по merch_uuid и query параметрам",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "string",
"description": "image type",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.ImageLink"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
"description": "Merch UUID",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "file",
"description": "Image file",
"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",
@ -118,11 +269,48 @@ const docTemplate = `{
"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": {
"$ref": "#/definitions/merch.MerchDTO"
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
@ -139,28 +327,425 @@ const docTemplate = `{
}
}
},
"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": "Обновить информацию про мерч по его uuid в json-е",
"tags": [
"Merch"
"description": "Изменить метку",
"consumes": [
"application/json"
],
"summary": "Обновить информацию про мерч",
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"$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"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить метку как удаленную",
"tags": [
"Merch labels"
],
"summary": "Пометить метку как удаленную",
"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"
}
}
}
}
},
"/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": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Получить всю информацию про мерч",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
@ -232,6 +817,9 @@ const docTemplate = `{
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -277,6 +865,9 @@ const docTemplate = `{
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -620,9 +1211,90 @@ 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": {
"etag": {
"type": "string"
},
"link": {
"type": "string"
}
}
},
"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"
},
@ -642,6 +1314,12 @@ const docTemplate = `{
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -706,6 +1384,43 @@ const docTemplate = `{
}
}
},
"merch.UpdateMerchDTO": {
"type": "object",
"properties": {
"link": {
"type": "string"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"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"
],
@ -87,20 +90,168 @@
}
}
}
},
"put": {
"security": [
{
"BearerAuth": []
}
],
"description": "Обновить информацию про мерч по его uuid в json-е",
"consumes": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Обновить информацию про мерч",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.UpdateMerchDTO"
}
}
],
"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}": {
"/merch/images/{uuid}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"tags": [
"Merch"
"description": "Получить картинки по merch_uuid и query параметрам",
"produces": [
"application/json"
],
"summary": "Получить всю информацию про мерч",
"tags": [
"Merch images"
],
"summary": "Получить картинки по merch_uuid и query параметрам",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "string",
"description": "image type",
"name": "type",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/merch.ImageLink"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse400"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/responses.ErrorResponse500"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на созданные картинки.",
"consumes": [
"multipart/form-data"
],
"produces": [
"application/json"
],
"tags": [
"Merch images"
],
"summary": "Загрузить картинку по merch_uuid",
"parameters": [
{
"type": "string",
"description": "Merch UUID",
"name": "uuid",
"in": "path",
"required": true
},
{
"type": "file",
"description": "Image file",
"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",
@ -110,11 +261,48 @@
"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": {
"$ref": "#/definitions/merch.MerchDTO"
"type": "array",
"items": {
"$ref": "#/definitions/merch.LabelsList"
}
}
},
"400": {
@ -131,28 +319,425 @@
}
}
},
"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": "Обновить информацию про мерч по его uuid в json-е",
"tags": [
"Merch"
"description": "Изменить метку",
"consumes": [
"application/json"
],
"summary": "Обновить информацию про мерч",
"tags": [
"Merch labels"
],
"summary": "Изменить метку",
"parameters": [
{
"description": "merch_uuid",
"name": "body",
"type": "string",
"description": "label uuid",
"name": "uuid",
"in": "path",
"required": true
},
{
"description": "payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/merch.MerchDTO"
"$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"
}
}
}
},
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Пометить метку как удаленную",
"tags": [
"Merch labels"
],
"summary": "Пометить метку как удаленную",
"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"
}
}
}
}
},
"/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": [
{
"BearerAuth": []
}
],
"description": "Получить всю информацию про мерч по его uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
"summary": "Получить всю информацию про мерч",
"parameters": [
{
"type": "string",
"description": "merch_uuid",
"name": "uuid",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
@ -224,6 +809,9 @@
}
],
"description": "Получить цены мерча за период",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -269,6 +857,9 @@
}
],
"description": "Получить перепады цен мерча за период по его merch_uuid",
"produces": [
"application/json"
],
"tags": [
"Merch"
],
@ -612,9 +1203,90 @@
}
},
"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": {
"etag": {
"type": "string"
},
"link": {
"type": "string"
}
}
},
"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"
},
@ -634,6 +1306,12 @@
"merch.MerchDTO": {
"type": "object",
"properties": {
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"merch_uuid": {
"type": "string"
},
@ -698,6 +1376,43 @@
}
}
},
"merch.UpdateMerchDTO": {
"type": "object",
"properties": {
"link": {
"type": "string"
},
"merch_uuid": {
"type": "string"
},
"name": {
"type": "string"
},
"origin": {
"type": "string"
}
}
},
"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,7 +1,59 @@
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:
type: string
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:
@ -14,6 +66,10 @@ definitions:
type: object
merch.MerchDTO:
properties:
labels:
items:
type: string
type: array
merch_uuid:
type: string
name:
@ -55,6 +111,30 @@ definitions:
link:
type: string
type: object
merch.UpdateMerchDTO:
properties:
link:
type: string
merch_uuid:
type: string
name:
type: string
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:
@ -153,6 +233,8 @@ paths:
/merch/:
get:
description: Получить все записи мерча
produces:
- application/json
responses:
"200":
description: OK
@ -173,6 +255,33 @@ paths:
summary: Получить все записи мерча
tags:
- Merch
put:
consumes:
- application/json
description: Обновить информацию про мерч по его uuid в json-е
parameters:
- description: merch_uuid
in: body
name: body
required: true
schema:
$ref: '#/definitions/merch.UpdateMerchDTO'
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
/merch/{uuid}:
delete:
description: Пометить мерч как удаленный по его uuid
@ -208,6 +317,8 @@ paths:
name: uuid
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -226,20 +337,18 @@ paths:
summary: Получить всю информацию про мерч
tags:
- Merch
put:
description: Обновить информацию про мерч по его uuid в json-е
/merch/images/{uuid}:
delete:
description: Удалить (безвозвратно) картинки по merch_uuid
parameters:
- description: merch_uuid
in: body
name: body
in: path
name: uuid
required: true
schema:
$ref: '#/definitions/merch.MerchDTO'
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/merch.MerchDTO'
"400":
description: Bad Request
schema:
@ -250,9 +359,351 @@ paths:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Обновить информацию про мерч
summary: Удалить (безвозвратно) картинки по merch_uuid
tags:
- Merch
- Merch images
get:
description: Получить картинки по merch_uuid и query параметрам
parameters:
- description: merch_uuid
in: path
name: uuid
required: true
type: string
- description: image type
in: query
name: type
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/merch.ImageLink'
"400":
description: Bad Request
schema:
$ref: '#/definitions/responses.ErrorResponse400'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/responses.ErrorResponse500'
security:
- BearerAuth: []
summary: Получить картинки по merch_uuid и query параметрам
tags:
- Merch images
post:
consumes:
- multipart/form-data
description: Загрузить картинку по merch_uuid. В ответ будут выданы ссылки на
созданные картинки.
parameters:
- description: Merch UUID
in: path
name: uuid
required: true
type: string
- description: Image file
in: formData
name: file
required: true
type: file
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:
- application/json
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: Получить метки товара по его uuid
tags:
- 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: Получить цены мерча за период
@ -261,6 +712,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK
@ -294,6 +747,8 @@ paths:
in: query
name: days
type: string
produces:
- application/json
responses:
"200":
description: OK

31
go.mod
View file

@ -3,16 +3,18 @@ module merch-parser-api
go 1.25.1
require (
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/minio/minio-go/v7 v7.0.95
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.42.0
google.golang.org/grpc v1.75.1
golang.org/x/crypto v0.43.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
@ -24,8 +26,10 @@ require (
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
@ -38,7 +42,7 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@ -48,25 +52,32 @@ 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.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
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/image v0.32.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
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-20251022142026-3a174f9686a8 // indirect
)

64
go.sum
View file

@ -11,6 +11,10 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
@ -21,6 +25,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -54,8 +60,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
@ -83,6 +89,9 @@ 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.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=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -93,6 +102,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -100,6 +115,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
@ -108,6 +125,8 @@ github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9M
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -126,6 +145,8 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
@ -147,21 +168,24 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
@ -174,8 +198,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -183,20 +207,20 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
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-20251002232023-7c0ddcbb5797 h1:CirRxTOwnRWVLKzDNrs0CXAaVozJoR4G9xvdRecrdpk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
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=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -1,29 +1,32 @@
package merch
import (
"context"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"merch-parser-api/internal/interfaces"
"merch-parser-api/pkg/responses"
"net/http"
"strings"
"time"
)
type controller struct {
service *service
utils interfaces.Utils
expires time.Duration
}
func newController(service *service, utils interfaces.Utils) *controller {
func newController(service *service, utils interfaces.Utils, expires time.Duration) *controller {
return &controller{
service: service,
utils: utils,
expires: expires,
}
}
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)
@ -34,6 +37,26 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref
chartsGroup.GET("", h.controller.getChartsPrices)
chartsGroup.GET("/:uuid", h.controller.getDistinctPrices)
imagesGroup := merchGroup.Group("/images")
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 Добавить новый мерч
@ -75,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
@ -108,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
@ -134,10 +159,11 @@ func (co *controller) getAllMerch(c *gin.Context) {
// @Description Обновить информацию про мерч по его uuid в json-е
// @Tags Merch
// @Security BearerAuth
// @Param body body UpdateMerchDTO true "merch_uuid"
// @Accept json
// @Param body body UpdateMerchDTO true "merch_uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/ [put]
func (co *controller) updateMerch(c *gin.Context) {
var payload UpdateMerchDTO
@ -159,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 Пометить мерч как удаленный
@ -169,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")
@ -197,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", ""))
@ -226,11 +259,12 @@ 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
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Param days query string false "period in days"
// @Success 200 {object} PricesResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /prices/{uuid} [get]
func (co *controller) getDistinctPrices(c *gin.Context) {
daysQuery := strings.ToLower(c.DefaultQuery("days", ""))
@ -257,3 +291,506 @@ func (co *controller) getDistinctPrices(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// @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"
// @Success 200 {object} imageStorage.UploadMerchImageResponse
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [post]
func (co *controller) uploadMerchImage(c *gin.Context) {
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.Error("Merch | Failed to get single merch")
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 {
c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "file is required"})
log.WithError(err).Error("Merch | File is required")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
//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.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
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [get]
func (co *controller) getMerchImage(c *gin.Context) {
//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)
}
// @Summary Удалить (безвозвратно) картинки по merch_uuid
// @Description Удалить (безвозвратно) картинки по merch_uuid
// @Tags Merch images
// @Security BearerAuth
// @Param uuid path string true "merch_uuid"
// @Success 200
// @Failure 400 {object} responses.ErrorResponse400
// @Failure 500 {object} responses.ErrorResponse500
// @Router /merch/images/{uuid} [delete]
func (co *controller) deleteMerchImage(c *gin.Context) {
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 merch uuid")
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), co.expires)
defer cancel()
//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 {
@ -27,8 +30,9 @@ type SingleMerchResponse struct {
}
type ListResponse struct {
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
MerchUuid string `json:"merch_uuid"`
Name string `json:"name"`
Labels []string `json:"labels,omitempty" gorm:"-"`
}
type PriceEntry struct {
@ -53,3 +57,39 @@ type UpdateMerchDTO struct {
Origin string `json:"origin"`
Link string `json:"link"`
}
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

@ -3,6 +3,8 @@ package merch
import (
"gorm.io/gorm"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"time"
)
type Handler struct {
@ -14,12 +16,33 @@ type Handler struct {
type Deps struct {
DB *gorm.DB
Utils interfaces.Utils
//Media interfaces.MediaStorage
ImageStorage is.ImageStorageClient
}
func NewHandler(deps Deps) *Handler {
packageBucketName := "user-merch-images"
expires := time.Minute * 5
r := NewRepo(deps.DB)
s := newService(r)
c := newController(s, deps.Utils)
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")
//}
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

@ -21,6 +21,8 @@ func NewRepo(db *gorm.DB) *Repo {
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)
@ -31,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 {
@ -54,6 +72,36 @@ func (r *Repo) addMerch(bundle merchBundle) error {
return nil
}
func (r *Repo) merchRecordExists(userUuid, merchUuid string) (bool, error) {
var exists bool
err := r.db.Raw(`
SELECT EXISTS (
SELECT 1
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.
@ -224,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

@ -1,22 +1,59 @@
package merch
import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"github.com/disintegration/imaging"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"image"
"image/jpeg"
"io"
"merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage"
"mime/multipart"
"path/filepath"
"strings"
"time"
)
type service struct {
repo repository
repo repository
media interfaces.MediaStorage
bucketName string
expires time.Duration
imageStorage is.ImageStorageClient
}
func newService(repo repository) *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,
repo: deps.repo,
media: deps.media,
bucketName: deps.bucketName,
expires: deps.expires,
imageStorage: deps.imageStorage,
}
}
type uploadImageParams struct {
ctx context.Context
src io.Reader
imageType string
object string
quality int
}
func (s *service) addMerch(payload MerchDTO, userUuid string) error {
merchUuid := uuid.NewString()
@ -64,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 {
@ -80,6 +144,13 @@ func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
}
func (s *service) deleteMerch(userUuid, merchUuid string) error {
ctx, cancel := context.WithTimeout(context.Background(), s.expires)
defer cancel()
if err := s.deleteMerchImage(ctx, userUuid, merchUuid); err != nil {
return err
}
return s.repo.deleteMerch(userUuid, merchUuid)
}
@ -176,3 +247,422 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
Origins: []OriginWithPrices{originSurugaya, originMandarake},
}, 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 {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
rawExt := filepath.Ext(file.Filename)
if rawExt == "" {
return errors.New("no file extension")
}
ext := strings.ToLower(rawExt[1:])
allowedTypes := map[string]struct{}{"jpeg": {}, "jpg": {}, "png": {}, "gif": {}}
if _, ok := allowedTypes[ext]; !ok {
return errors.New("invalid file type")
}
getSrc := func() (io.ReadCloser, error) {
f, err := file.Open()
if err != nil {
log.WithError(err).Error("Merch | Failed to open file")
return nil, err
}
return f, nil
}
switch imageType {
case "thumbnail":
src, err := getSrc()
if err != nil {
return err
}
return s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "thumbnail",
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
quality: 80,
})
case "full":
src, err := getSrc()
if err != nil {
return err
}
return s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "full",
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
quality: 90,
})
case "all":
src, err := getSrc()
if err != nil {
return err
}
if err = s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src,
imageType: "thumbnail",
object: fmt.Sprintf("%s/merch/%s/thumbnail.jpg", userUuid, merchUuid),
quality: 80,
}); err != nil {
log.WithError(err).Error("Merch | Upload thumbnail and full image")
return err
}
src2, err := getSrc()
if err != nil {
return err
}
if err = s._uploadToStorage(uploadImageParams{
ctx: ctx,
src: src2,
imageType: "full",
object: fmt.Sprintf("%s/merch/%s/full.jpg", userUuid, merchUuid),
quality: 90,
}); err != nil {
log.WithError(err).Error("Merch | Upload thumbnail and full image")
return err
}
default:
return errors.New("invalid file type")
}
return nil
}
// 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
}
if !exists {
return ImageLink{}, fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
object, err := s.makeObject(userUuid, merchUuid, imageType)
if err != nil {
return ImageLink{}, err
}
link, err := s.media.GetPresignedLink(ctx, s.bucketName, object, s.expires, nil)
if err != nil {
return ImageLink{}, err
}
etag, err := s.media.GetObjectEtag(ctx, s.bucketName, object)
if err != nil {
return ImageLink{}, err
}
return ImageLink{
Link: link,
ETag: etag,
}, 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 {
return err
}
if !exists {
return fmt.Errorf("no merch found for user %s with uuid %s", userUuid, merchUuid)
}
//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
}
func (s *service) _uploadToStorage(params uploadImageParams) error {
img, _, err := image.Decode(params.src)
if err != nil {
return fmt.Errorf("failed to decode image: %w", err)
}
if params.imageType == "thumbnail" {
img = imaging.Resize(img, 300, 300, imaging.Lanczos)
}
var buf bytes.Buffer
if err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: params.quality}); err != nil {
return fmt.Errorf("failed to encode full image: %w", err)
}
err = s.media.Upload(params.ctx, s.bucketName, params.object, &buf, -1)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"img type": params.imageType,
}).Error("Merch | Failed to upload file to media storage")
return err
}
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

@ -0,0 +1,19 @@
package interfaces
import (
"context"
"io"
"net/url"
"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
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

@ -0,0 +1,42 @@
package mediaStorage
import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
log "github.com/sirupsen/logrus"
)
type Handler struct {
*Service
}
type Deps struct {
Endpoint string
User string
Password string
Secure string
}
func NewHandler(deps Deps) *Handler {
secureMode := false
if deps.Secure == "true" {
secureMode = true
}
minioClient, err := minio.New(deps.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(deps.User, deps.Password, ""),
Secure: secureMode,
})
if err != nil {
log.WithError(err).Fatal("Media storage | Failed to create minio client")
}
log.WithFields(log.Fields{
"endpoint": deps.Endpoint,
"secure": secureMode,
}).Debug("Media storage | Created minio client")
return &Handler{
newService(minioClient, deps.Endpoint, secureMode),
}
}

View file

@ -0,0 +1,89 @@
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
endpoint string
secureMode bool
}
func newService(client *minio.Client, endpoint string, secureMode bool) *Service {
return &Service{
client: client,
endpoint: endpoint,
secureMode: secureMode,
}
}
func (s *Service) CheckBucketExists(bucketName string) (bool, error) {
ctx := context.Background()
exists, err := s.client.BucketExists(ctx, bucketName)
if err != nil {
log.WithError(err).Fatal("Media storage | Failed to check bucket existence")
return exists, err
}
log.Infof("Media storage | Bucket %s exists", bucketName)
return exists, nil
}
func (s *Service) Upload(ctx context.Context, bucket, object string, reader io.Reader, size int64) error {
_, err := s.client.PutObject(ctx, bucket, object, reader, size, minio.PutObjectOptions{ContentType: "image/jpeg"})
return err
}
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
}
return presigned.String(), nil
}
func (s *Service) Delete(ctx context.Context, bucket, object string) error {
return s.client.RemoveObject(ctx, bucket, object, minio.RemoveObjectOptions{})
}
func (s *Service) GetObjectEtag(ctx context.Context, bucketName, object string) (string, error) {
info, err := s.client.StatObject(ctx, bucketName, object, minio.StatObjectOptions{})
if err != nil {
return "", err
}
return info.ETag, nil
}

View file

@ -55,4 +55,27 @@ CREATE TABLE prices(
merch_uuid VARCHAR(36) NOT NULL,
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",
}