refactor + new origin

This commit is contained in:
nquidox 2025-12-15 17:34:01 +03:00
parent 8f2b0470b1
commit e0e5761cd6
10 changed files with 228 additions and 115 deletions

View file

@ -1,26 +1,39 @@
package merch package merch
import "time" import (
"merch-parser-api/internal/api/merch/origins"
"time"
)
type merchBundle struct { type merchBundle struct {
Merch *Merch Merch *Merch
Surugaya *Surugaya Surugaya *origins.OriginSurugaya
Mandarake *Mandarake Mandarake *origins.OriginMandarake
Amiami *origins.OriginAmiami
} }
type MerchDTO struct { type MerchDTO struct {
MerchUuid string `json:"merch_uuid"` MerchUuid string `json:"merch_uuid"`
Name string `json:"name"` Name string `json:"name"`
OriginSurugaya SurugayaDTO `json:"origin_surugaya"` OriginSurugaya SurugayaDTO `json:"origin_surugaya"`
OriginMandarake MandarakeDTO `json:"origin_mandarake"` OriginMandarake MandarakeDTO `json:"origin_mandarake"`
OriginAmiami AmiamiDTO `json:"origin_amiami"`
Labels []string `json:"labels,omitempty" gorm:"-"` Labels []string `json:"labels,omitempty" gorm:"-"`
} }
type SurugayaDTO struct { type OriginDTO struct {
Link string `json:"link"` Link string `json:"link"`
} }
type SurugayaDTO struct {
OriginDTO
}
type MandarakeDTO struct { type MandarakeDTO struct {
Link string `json:"link"` OriginDTO
}
type AmiamiDTO struct {
OriginDTO
} }
type SingleMerchResponse struct { type SingleMerchResponse struct {
@ -41,7 +54,7 @@ type PriceEntry struct {
} }
type OriginWithPrices struct { type OriginWithPrices struct {
Origin Origin `json:"origin"` Origin origins.Origin `json:"origins"`
Prices []PriceEntry Prices []PriceEntry
} }
@ -54,7 +67,7 @@ type PricesResponse struct {
type UpdateMerchDTO struct { type UpdateMerchDTO struct {
MerchUuid string `json:"merch_uuid"` MerchUuid string `json:"merch_uuid"`
Name string `json:"name"` Name string `json:"name"`
Origin string `json:"origin"` Origin string `json:"origins"`
Link string `json:"link"` Link string `json:"link"`
} }
@ -86,7 +99,7 @@ type ZeroPrice struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
MerchUuid string `json:"merch_uuid"` MerchUuid string `json:"merch_uuid"`
Name string `json:"name"` Name string `json:"name"`
Origin Origin `json:"origin"` Origin origins.Origin `json:"origins"`
} }
type DeleteZeroPrices struct { type DeleteZeroPrices struct {

View file

@ -2,6 +2,7 @@ package merch
import ( import (
"database/sql" "database/sql"
"merch-parser-api/internal/api/merch/origins"
"time" "time"
) )
@ -19,28 +20,6 @@ func (Merch) TableName() string {
return "merch" return "merch"
} }
type Surugaya struct {
Id uint `gorm:"primary_key" json:"-"`
DeletedAt sql.NullTime `json:"-"`
MerchUuid string `json:"-"`
Link string `json:"link"`
}
func (Surugaya) TableName() string {
return "origin_surugaya"
}
type Mandarake struct {
Id uint `gorm:"primary_key" json:"-"`
DeletedAt sql.NullTime `json:"-"`
MerchUuid string `json:"-"`
Link string `json:"link"`
}
func (Mandarake) TableName() string {
return "origin_mandarake"
}
type Price struct { type Price struct {
Id uint `json:"id" gorm:"primary_key"` Id uint `json:"id" gorm:"primary_key"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
@ -48,7 +27,7 @@ type Price struct {
DeletedAt sql.NullTime `json:"deleted_at,omitempty" gorm:"column:deleted_at"` DeletedAt sql.NullTime `json:"deleted_at,omitempty" gorm:"column:deleted_at"`
MerchUuid string `json:"merch_uuid" gorm:"column:merch_uuid"` MerchUuid string `json:"merch_uuid" gorm:"column:merch_uuid"`
Price int `json:"price" gorm:"column:price"` Price int `json:"price" gorm:"column:price"`
Origin Origin `json:"origin" gorm:"column:origin;type:integer"` Origin origins.Origin `json:"origins" gorm:"column:origins;type:integer"`
} }
type Label struct { type Label struct {

View file

@ -0,0 +1,55 @@
package origins
import "database/sql"
func AllOriginModels() []interface{} {
return []interface{}{
&OriginSurugaya{},
&OriginMandarake{},
&OriginAmiami{},
}
}
func NewSurugaya(merchUuid, link string) *OriginSurugaya {
return &OriginSurugaya{
BaseOrigin{
DeletedAt: sql.NullTime{},
MerchUuid: merchUuid,
Link: link,
},
}
}
func NewMandarake(merchUuid, link string) *OriginMandarake {
return &OriginMandarake{
BaseOrigin{
DeletedAt: sql.NullTime{},
MerchUuid: merchUuid,
Link: link,
},
}
}
func NewAmiami(merchUuid, link string) *OriginAmiami {
return &OriginAmiami{
BaseOrigin{
DeletedAt: sql.NullTime{},
MerchUuid: merchUuid,
Link: link,
},
}
}
type OriginFactory func(merchUuid, link string) interface{}
var OriginFactories = map[string]OriginFactory{
"surugaya": func(merchUuid, link string) interface{} {
return &OriginSurugaya{BaseOrigin: BaseOrigin{MerchUuid: merchUuid, Link: link}}
},
"mandarake": func(merchUuid, link string) interface{} {
return &OriginMandarake{BaseOrigin: BaseOrigin{MerchUuid: merchUuid, Link: link}}
},
"amiami": func(merchUuid, link string) interface{} {
return &OriginAmiami{BaseOrigin: BaseOrigin{MerchUuid: merchUuid, Link: link}}
},
}

View file

@ -0,0 +1,34 @@
package origins
import "database/sql"
type BaseOrigin struct {
Id uint `gorm:"primary_key" json:"-"`
DeletedAt sql.NullTime `json:"-"`
MerchUuid string `json:"-"`
Link string `json:"link"`
}
type OriginSurugaya struct {
BaseOrigin
}
func (OriginSurugaya) TableName() string {
return "origin_surugaya"
}
type OriginMandarake struct {
BaseOrigin
}
func (OriginMandarake) TableName() string {
return "origin_mandarake"
}
type OriginAmiami struct {
BaseOrigin
}
func (OriginAmiami) TableName() string {
return "origin_amiami"
}

View file

@ -1,4 +1,4 @@
package merch package origins
import ( import (
"database/sql/driver" "database/sql/driver"
@ -9,13 +9,15 @@ import (
type Origin int type Origin int
const ( const (
surugaya = (iota + 1) * 1000 Surugaya = (iota + 1) * 1000
mandarake Mandarake
Amiami
) )
var Origins = [...]string{ var Origins = [...]string{
"surugaya", "surugaya",
"mandarake", "mandarake",
"amiami",
} }
func (o Origin) String() string { func (o Origin) String() string {
@ -30,7 +32,7 @@ func (o Origin) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String()) return json.Marshal(o.String())
} }
func parseOrigin(s string) (Origin, bool) { func ParseOrigin(s string) (Origin, bool) {
for i, name := range Origins { for i, name := range Origins {
if name == strings.ToLower(s) { if name == strings.ToLower(s) {
return Origin((i + 1) * 1000), true return Origin((i + 1) * 1000), true

View file

@ -4,13 +4,14 @@ import (
"database/sql" "database/sql"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
"merch-parser-api/internal/api/merch/origins"
"merch-parser-api/internal/shared" "merch-parser-api/internal/shared"
"time" "time"
) )
type Link struct { type Link struct {
Surugaya []Surugaya Surugaya []origins.OriginSurugaya
Mandarake []Mandarake Mandarake []origins.OriginMandarake
} }
type TaskProvider struct { type TaskProvider struct {
@ -84,7 +85,7 @@ func (p *TaskProvider) InsertPrices(prices []shared.TaskResult) error {
var insertPrices []Price var insertPrices []Price
for _, item := range prices { for _, item := range prices {
origin, ok := parseOrigin(item.Origin) origin, ok := origins.ParseOrigin(item.Origin)
if !ok { if !ok {
continue continue
} }
@ -107,13 +108,13 @@ func (p *TaskProvider) InsertPrices(prices []shared.TaskResult) error {
} }
func (r *TaskRepo) getLinks() (*Link, error) { func (r *TaskRepo) getLinks() (*Link, error) {
var surugayaList []Surugaya var surugayaList []origins.OriginSurugaya
if err := r.db.Model(&Surugaya{}).Where("deleted_at IS NULL").Find(&surugayaList).Error; err != nil { if err := r.db.Model(&origins.OriginSurugaya{}).Where("deleted_at IS NULL").Find(&surugayaList).Error; err != nil {
return nil, err return nil, err
} }
var mandarakeList []Mandarake var mandarakeList []origins.OriginMandarake
if err := r.db.Model(&Mandarake{}).Where("deleted_at IS NULL").Find(&mandarakeList).Error; err != nil { if err := r.db.Model(&origins.OriginMandarake{}).Where("deleted_at IS NULL").Find(&mandarakeList).Error; err != nil {
return nil, err return nil, err
} }

View file

@ -3,8 +3,10 @@ package merch
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
"merch-parser-api/internal/api/merch/origins"
"time" "time"
) )
@ -61,11 +63,15 @@ func (r *Repo) addMerch(bundle merchBundle) error {
return err return err
} }
if err := r.db.Model(&Surugaya{}).Create(bundle.Surugaya).Error; err != nil { if err := r.db.Model(&origins.OriginSurugaya{}).Create(bundle.Surugaya).Error; err != nil {
return err return err
} }
if err := r.db.Model(&Mandarake{}).Create(bundle.Mandarake).Error; err != nil { if err := r.db.Model(&origins.OriginMandarake{}).Create(bundle.Mandarake).Error; err != nil {
return err
}
if err := r.db.Model(&origins.OriginAmiami{}).Create(bundle.Amiami).Error; err != nil {
return err return err
} }
@ -112,7 +118,7 @@ func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
return merchBundle{}, err return merchBundle{}, err
} }
var surugaya Surugaya var surugaya origins.OriginSurugaya
if err := r.db. if err := r.db.
Where("merch_uuid = ?", merchUuid). Where("merch_uuid = ?", merchUuid).
First(&surugaya).Error; err != nil { First(&surugaya).Error; err != nil {
@ -121,7 +127,7 @@ func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
} }
} }
var mandarake Mandarake var mandarake origins.OriginMandarake
if err := r.db. if err := r.db.
Where("merch_uuid = ?", merchUuid). Where("merch_uuid = ?", merchUuid).
First(&mandarake).Error; err != nil { First(&mandarake).Error; err != nil {
@ -130,10 +136,20 @@ func (r *Repo) getSingleMerch(userUuid, merchUuid string) (merchBundle, error) {
} }
} }
var amiami origins.OriginAmiami
if err := r.db.
Where("merch_uuid = ?", merchUuid).
First(&amiami).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return merchBundle{}, err
}
}
return merchBundle{ return merchBundle{
Merch: &merch, Merch: &merch,
Surugaya: &surugaya, Surugaya: &surugaya,
Mandarake: &mandarake, Mandarake: &mandarake,
Amiami: &amiami,
}, nil }, nil
} }
@ -164,22 +180,13 @@ func (r *Repo) updateMerch(payload UpdateMerchDTO, userUuid string) error {
return err return err
} }
switch payload.Origin { if factory, ok := origins.OriginFactories[payload.Origin]; ok {
case "surugaya": model := factory(payload.MerchUuid, payload.Link)
if err := r.upsertOrigin(&Surugaya{ if err := r.upsertOrigin(model); err != nil {
MerchUuid: payload.MerchUuid,
Link: payload.Link,
}); err != nil {
return err
}
case "mandarake":
if err := r.upsertOrigin(&Mandarake{
MerchUuid: payload.MerchUuid,
Link: payload.Link,
}); err != nil {
return err return err
} }
} else if payload.Origin != "" {
return fmt.Errorf("unsupported origin: %s", payload.Origin)
} }
return nil return nil
@ -196,18 +203,15 @@ func (r *Repo) deleteMerch(userUuid, merchUuid string) error {
return err return err
} }
ors := origins.AllOriginModels()
for _, origin := range ors {
if err := r.db. if err := r.db.
Model(&Surugaya{}). Model(origin).
Where("merch_uuid = ?", merchUuid). Where("merch_uuid = ?", merchUuid).
Update("deleted_at", now).Error; err != nil { Update("deleted_at", now).Error; err != nil {
return err return err
} }
if err := r.db.
Model(&Mandarake{}).
Where("merch_uuid = ?", merchUuid).
Update("deleted_at", now).Error; err != nil {
return err
} }
return nil return nil
@ -226,7 +230,7 @@ func (r *Repo) getAllUserMerch(userUuid string) (merchList []Merch, err error) {
func (r *Repo) getPricesWithDays(userUuid string, period time.Time) (prices []Price, err error) { func (r *Repo) getPricesWithDays(userUuid string, period time.Time) (prices []Price, err error) {
err = r.db.Raw(` err = r.db.Raw(`
SELECT p.created_at, p.merch_uuid, p.price, p.origin SELECT p.created_at, p.merch_uuid, p.price, p.origins
FROM prices AS p FROM prices AS p
JOIN merch AS m ON m.merch_uuid = p.merch_uuid JOIN merch AS m ON m.merch_uuid = p.merch_uuid
WHERE m.user_uuid = ? WHERE m.user_uuid = ?
@ -244,9 +248,9 @@ func (r *Repo) getPricesWithDays(userUuid string, period time.Time) (prices []Pr
func (r *Repo) getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error) { func (r *Repo) getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error) {
err = r.db.Raw(` err = r.db.Raw(`
SELECT price, created_at, origin SELECT price, created_at, origins
FROM ( FROM (
SELECT DISTINCT ON (price) price, created_at, origin SELECT DISTINCT ON (price) price, created_at, origins
FROM prices FROM prices
WHERE merch_uuid = ? WHERE merch_uuid = ?
AND created_at > ? AND created_at > ?
@ -338,9 +342,9 @@ func (r *Repo) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
if err := r.db.Raw(` if err := r.db.Raw(`
WITH price_with_neighbors AS ( WITH price_with_neighbors AS (
SELECT SELECT
p.id, p.created_at, p.merch_uuid, p.price, p.origin, m.name, p.id, p.created_at, p.merch_uuid, p.price, p.origins, m.name,
LAG(price) OVER (PARTITION BY p.merch_uuid, p.origin ORDER BY p.created_at, p.id) AS prev_price, LAG(price) OVER (PARTITION BY p.merch_uuid, p.origins 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 LEAD(price) OVER (PARTITION BY p.merch_uuid, p.origins ORDER BY p.created_at, p.id) AS next_price
FROM prices AS p FROM prices AS p
JOIN merch as m ON m.merch_uuid = p.merch_uuid JOIN merch as m ON m.merch_uuid = p.merch_uuid
WHERE p.deleted_at IS NULL WHERE p.deleted_at IS NULL
@ -348,7 +352,7 @@ func (r *Repo) getZeroPrices(userUuid string) ([]ZeroPrice, error) {
AND m.user_uuid = ?) AND m.user_uuid = ?)
SELECT SELECT
id, created_at, merch_uuid, origin, name id, created_at, merch_uuid, origins, name
FROM price_with_neighbors FROM price_with_neighbors
WHERE WHERE
price = 0 price = 0

View file

@ -12,6 +12,7 @@ import (
"image" "image"
"image/jpeg" "image/jpeg"
"io" "io"
"merch-parser-api/internal/api/merch/origins"
"merch-parser-api/internal/interfaces" "merch-parser-api/internal/interfaces"
is "merch-parser-api/proto/imageStorage" is "merch-parser-api/proto/imageStorage"
"mime/multipart" "mime/multipart"
@ -58,7 +59,7 @@ func (s *service) addMerch(payload MerchDTO, userUuid string) error {
merchUuid := uuid.NewString() merchUuid := uuid.NewString()
bundle := merchBundle{ bundle := merchBundle{
&Merch{ Merch: &Merch{
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: sql.NullTime{Valid: false}, UpdatedAt: sql.NullTime{Valid: false},
DeletedAt: sql.NullTime{Valid: false}, DeletedAt: sql.NullTime{Valid: false},
@ -67,17 +68,9 @@ func (s *service) addMerch(payload MerchDTO, userUuid string) error {
Name: payload.Name, Name: payload.Name,
}, },
&Surugaya{ Surugaya: origins.NewSurugaya(merchUuid, payload.OriginSurugaya.Link),
DeletedAt: sql.NullTime{}, Mandarake: origins.NewMandarake(merchUuid, payload.OriginMandarake.Link),
MerchUuid: merchUuid, Amiami: origins.NewAmiami(merchUuid, payload.OriginAmiami.Link),
Link: payload.OriginSurugaya.Link,
},
&Mandarake{
DeletedAt: sql.NullTime{},
MerchUuid: merchUuid,
Link: payload.OriginMandarake.Link,
},
} }
return s.repo.addMerch(bundle) return s.repo.addMerch(bundle)
} }
@ -92,11 +85,20 @@ func (s *service) getSingleMerch(userUuid, merchUuid string) (MerchDTO, error) {
MerchUuid: bundle.Merch.MerchUuid, MerchUuid: bundle.Merch.MerchUuid,
Name: bundle.Merch.Name, Name: bundle.Merch.Name,
OriginSurugaya: SurugayaDTO{ OriginSurugaya: SurugayaDTO{
OriginDTO{
Link: bundle.Surugaya.Link, Link: bundle.Surugaya.Link,
}, },
},
OriginMandarake: MandarakeDTO{ OriginMandarake: MandarakeDTO{
OriginDTO{
Link: bundle.Mandarake.Link, Link: bundle.Mandarake.Link,
}, },
},
OriginAmiami: AmiamiDTO{
OriginDTO{
Link: bundle.Amiami.Link,
},
},
}, nil }, nil
} }
@ -137,7 +139,7 @@ func (s *service) updateMerch(payload UpdateMerchDTO, userUuid string) error {
} }
if payload.Origin == "" { if payload.Origin == "" {
return errors.New("no origin provided") return errors.New("no origins provided")
} }
return s.repo.updateMerch(payload, userUuid) return s.repo.updateMerch(payload, userUuid)
@ -178,10 +180,10 @@ func (s *service) getPrices(userUuid string, days string) ([]PricesResponse, err
return nil, err return nil, err
} }
pricesMap := make(map[string]map[Origin][]PriceEntry) pricesMap := make(map[string]map[origins.Origin][]PriceEntry)
for _, item := range pricesList { for _, item := range pricesList {
if _, ok := pricesMap[item.MerchUuid]; !ok { if _, ok := pricesMap[item.MerchUuid]; !ok {
pricesMap[item.MerchUuid] = make(map[Origin][]PriceEntry) pricesMap[item.MerchUuid] = make(map[origins.Origin][]PriceEntry)
} }
pricesMap[item.MerchUuid][item.Origin] = append(pricesMap[item.MerchUuid][item.Origin], PriceEntry{ pricesMap[item.MerchUuid][item.Origin] = append(pricesMap[item.MerchUuid][item.Origin], PriceEntry{
@ -192,16 +194,22 @@ func (s *service) getPrices(userUuid string, days string) ([]PricesResponse, err
for i := range response { for i := range response {
originSurugaya := OriginWithPrices{ originSurugaya := OriginWithPrices{
Origin: surugaya, Origin: origins.Surugaya,
Prices: pricesMap[response[i].MerchUuid][surugaya], Prices: pricesMap[response[i].MerchUuid][origins.Surugaya],
} }
response[i].Origins = append(response[i].Origins, originSurugaya) response[i].Origins = append(response[i].Origins, originSurugaya)
originMandarake := OriginWithPrices{ originMandarake := OriginWithPrices{
Origin: mandarake, Origin: origins.Mandarake,
Prices: pricesMap[response[i].MerchUuid][mandarake], Prices: pricesMap[response[i].MerchUuid][origins.Mandarake],
} }
response[i].Origins = append(response[i].Origins, originMandarake) response[i].Origins = append(response[i].Origins, originMandarake)
originAmiami := OriginWithPrices{
Origin: origins.Amiami,
Prices: pricesMap[response[i].MerchUuid][origins.Amiami],
}
response[i].Origins = append(response[i].Origins, originAmiami)
} }
return response, nil return response, nil
@ -218,27 +226,37 @@ func (s *service) getDistinctPrices(userUuid, merchUuid, days string) (PricesRes
} }
originSurugaya := OriginWithPrices{ originSurugaya := OriginWithPrices{
Origin: surugaya, Origin: origins.Surugaya,
Prices: []PriceEntry{}, Prices: []PriceEntry{},
} }
originMandarake := OriginWithPrices{ originMandarake := OriginWithPrices{
Origin: mandarake, Origin: origins.Mandarake,
Prices: []PriceEntry{},
}
originAmiami := OriginWithPrices{
Origin: origins.Amiami,
Prices: []PriceEntry{}, Prices: []PriceEntry{},
} }
for _, item := range result { for _, item := range result {
switch item.Origin { switch item.Origin {
case surugaya: case origins.Surugaya:
originSurugaya.Prices = append(originSurugaya.Prices, PriceEntry{ originSurugaya.Prices = append(originSurugaya.Prices, PriceEntry{
CreatedAt: item.CreatedAt.Unix(), CreatedAt: item.CreatedAt.Unix(),
Value: item.Price, Value: item.Price,
}) })
case mandarake: case origins.Mandarake:
originMandarake.Prices = append(originMandarake.Prices, PriceEntry{ originMandarake.Prices = append(originMandarake.Prices, PriceEntry{
CreatedAt: item.CreatedAt.Unix(), CreatedAt: item.CreatedAt.Unix(),
Value: item.Price, Value: item.Price,
}) })
case origins.Amiami:
originAmiami.Prices = append(originAmiami.Prices, PriceEntry{
CreatedAt: item.CreatedAt.Unix(),
Value: item.Price,
})
} }
} }

View file

@ -46,6 +46,13 @@ CREATE TABLE origin_mandarake(
link TEXT link TEXT
); );
CREATE TABLE origin_amiami(
id BIGSERIAL PRIMARY KEY,
deleted_at TIMESTAMP WITH TIME ZONE NULL,
merch_uuid VARCHAR(36) NOT NULL UNIQUE,
link TEXT
);
CREATE TABLE prices( CREATE TABLE prices(
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,