Initial commit;

This commit is contained in:
2026-01-17 22:45:02 +03:00
commit 2b8c774d0a
34 changed files with 1728 additions and 0 deletions

68
internal/app/app.go Normal file
View File

@@ -0,0 +1,68 @@
package app
import (
"context"
"fmt"
"os"
"steam_analyzer/internal/config"
"steam_analyzer/internal/handler"
"steam_analyzer/internal/repository"
"steam_analyzer/internal/service"
"steam_analyzer/internal/steamreq"
"steam_analyzer/internal/utils"
"steam_analyzer/internal/utils/slogutils"
"steam_analyzer/pkg/logger"
"steam_analyzer/pkg/postgresql"
"steam_analyzer/pkg/server"
"github.com/gorilla/mux"
)
var steamItemURL = `https://steamcommunity.com/market/search/render/`
func Run() {
cfg := config.MustLoadYaml("./config/config.dev.yaml")
logger := logger.New(cfg.Enviroment)
// db := repository.ConnectOrCreateDatabase()
// repo := repository.NewSQLLiteDatabase(db)
ctx := context.Background()
pool, err := postgresql.NewPGXPool(ctx, postgresql.CreateDBConnectionString(cfg.Database))
if err != nil {
logger.Error("Error while creating pool", slogutils.Error(err))
os.Exit(1)
}
repo := repository.NewPostgresDB(pool)
err = repo.InitDatabaseTables(ctx)
if err != nil {
logger.Error("Error while init tables", slogutils.Error(err))
os.Exit(1)
}
itemService := service.NewItemService(logger, repo)
requester := steamreq.NewRequester(steamItemURL, cfg.SteamRequester.RequestPerTime, cfg.SteamRequester.CountPerTime)
steamWorker := steamreq.NewSteamWorker(logger, requester, itemService, cfg.SteamWorker.RequestPerTime)
steamItems := []steamreq.SteamItems{
{
Query: "case",
ItemType: "case",
},
}
go steamWorker.Start(ctx, steamItems)
router := mux.NewRouter()
steamHandler := handler.NewSteamHandler(itemService)
steamHandler.RegisterRoutes(router, "/steam")
server := server.New(cfg.Server, logger, router)
server.Run()
}
func PrintItems(items []steamreq.SteamMarketItem) {
for _, item := range items {
item.SellPrice = float64(item.SellPrice) / 100.0 * utils.CURRENCY_RUB
fmt.Println(item.Name, item.HashName, item.SellPrice, item.SellListings, item.AssetDescription.Classid)
}
}

76
internal/config/config.go Normal file
View File

@@ -0,0 +1,76 @@
package config
import (
"log"
"os"
"time"
"github.com/ilyakaznacheev/cleanenv"
)
type Config struct {
Enviroment string `yaml:"enviroment" env:"ENVIROMENT" env-default:"local"`
Server Server `yaml:"server"`
Database Database `yaml:"database"`
SteamRequester SteamRequester `yaml:"steam_requester"`
SteamWorker SteamWorker `yaml:"steam_worker"`
}
type (
Server struct {
Host string `yaml:"host" env-default:"localhost"`
Port string `yaml:"port" env-default:"8080"`
IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"30s"`
WriteTimeout time.Duration `yaml:"write_timeout" env-default:"1s"`
ReadTimeout time.Duration `yaml:"read_timeout" env-default:"1s"`
Origins []string `yaml:"origins" env-separator:"," env-default:"http://localhost:3000,"`
}
Database struct {
Host string `yaml:"host" env:"DATABASE_HOST" env-description:"Database host" env-default:"localhost"`
Port string `yaml:"port" env:"DATABASE_PORT" env-description:"Database port" env-default:"5432"`
Sslmode string `yaml:"sslmode" env-description:"Database ssl mode" env-default:"disable"`
Name string `yaml:"name" env-description:"Database name"`
User string `yaml:"user" env-description:"Database user"`
Password string `yaml:"password" env-description:"Database user password"`
MaxConnAttempts int `yaml:"max_conn_attempt" env-default:"5"`
ConnTimeout time.Duration `yaml:"conn_timeout" env-default:"10s"`
MigrationPath string `yaml:"migration_path" env:"DATABASE_MIGRATION_PATH" env-description:"Migration path"`
}
SteamRequester struct {
RequestPerTime time.Duration `yaml:"request_per_time" env-description:"request limit per time" env-default:"5s"`
CountPerTime int `yaml:"count_per_time" env-description:"count limit per time" env-default:"1"`
}
SteamWorker struct {
RequestPerTime time.Duration `yaml:"request_per_time" env-description:"update prices time" env-default:"5m"`
}
)
func MustLoadYaml(configPath string) *Config {
if configPath == "" {
log.Fatal("config path is not set")
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Fatalf("config path doesn't exist: %s", configPath)
}
var cfg Config
if err := cleanenv.ReadConfig(configPath, &cfg); err != nil {
log.Fatalf("cannot read config: %s", err)
}
return &cfg
}
func MustLoadEnv() *Config {
var cfg Config
if err := cleanenv.ReadEnv(&cfg); err != nil {
log.Fatalf("cannot read config: %s", err)
}
return &cfg
}

55
internal/domain/item.go Normal file
View File

@@ -0,0 +1,55 @@
package domain
import (
"time"
"github.com/google/uuid"
)
type SteamItem struct {
ID uuid.UUID
Name string
ClassID string
GameID int
Price float64
Count int
History []ItemPriceHistory
}
func (si SteamItem) ToDB() DBItem {
return DBItem{
ID: si.ID,
Name: si.Name,
ClassID: si.ClassID,
GameID: si.GameID,
}
}
func (si SteamItem) ToDBHistory() DBItemHistory {
return DBItemHistory{
ItemID: si.ID,
Price: si.Price,
Count: si.Count,
Date: time.Now(),
}
}
type ItemPriceHistory struct {
Price float64
Count int
Date time.Time
}
type DBItem struct {
ID uuid.UUID
Name string
ClassID string
GameID int
}
type DBItemHistory struct {
ItemID uuid.UUID
Price float64
Count int
Date time.Time
}

View File

@@ -0,0 +1,58 @@
package handler
import (
"context"
"fmt"
"net/http"
"steam_analyzer/internal/repository"
"steam_analyzer/internal/utils/jsonutils"
"steam_analyzer/pkg/utils/httputils"
"github.com/gorilla/mux"
)
type ItemService interface {
GetAllItemsWithLastPrice(ctx context.Context, itemName string) ([]repository.ItemWithLastPrice, error)
// GetAllItemsWithLastPrice(itemName string) ([]*domain.SteamItem, error)
}
type SteamHandler struct {
itemService ItemService
}
func NewSteamHandler(itemService ItemService) SteamHandler {
return SteamHandler{
itemService: itemService,
}
}
func (h SteamHandler) RegisterRoutes(router *mux.Router, prefix string) {
steamRouter := router.PathPrefix(prefix)
getRouter := steamRouter.Methods(http.MethodGet).Subrouter()
getRouter.HandleFunc("/items", h.GetAllItemsWithLastPrice)
}
type GetAllItemsWithLastPriceResponse struct {
Result []repository.ItemWithLastPrice `json:"result"`
}
func (h SteamHandler) GetAllItemsWithLastPrice(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
w.WriteHeader(http.StatusBadRequest)
jsonutils.ToJSON(w, httputils.HTTPError{Message: "Name query param couldn't be empty"})
return
}
items, err := h.itemService.GetAllItemsWithLastPrice(r.Context(), name)
if err != nil {
fmt.Println(err)
return
}
w.Header().Add("Content-Type", "application/json")
jsonutils.ToJSON(w, GetAllItemsWithLastPriceResponse{Result: items})
}

View File

@@ -0,0 +1,202 @@
package repository
import (
"context"
"fmt"
"steam_analyzer/internal/domain"
"steam_analyzer/pkg/postgresql"
"github.com/google/uuid"
)
type PostgresDB struct {
conn postgresql.PGXPool
}
func NewPostgresDB(conn postgresql.PGXPool) *PostgresDB {
return &PostgresDB{
conn: conn,
}
}
func (db PostgresDB) InitDatabaseTables(ctx context.Context) error {
createTable := `
CREATE TABLE IF NOT EXISTS item (
id UUID PRIMARY KEY,
name TEXT,
class_id TEXT UNIQUE NOT NULL,
game_id INTEGER NOT NULL,
type_id INTEGER
);`
_, err := db.conn.Exec(ctx, createTable)
if err != nil {
return err
}
createTable = `
CREATE TABLE IF NOT EXISTS item_history (
item_id UUID REFERENCES item(id),
price REAL,
count INTEGER,
date TIMESTAMPTZ NOT NULL,
PRIMARY KEY (item_id, date)
);`
_, err = db.conn.Exec(ctx, createTable)
if err != nil {
return err
}
return nil
}
func (db PostgresDB) AddItem(ctx context.Context, item domain.DBItem) error {
query := `
INSERT INTO item (id, name, class_id, game_id)
VALUES ($1, $2, $3, $4)
`
_, err := db.conn.Exec(ctx, query,
item.ID,
item.Name,
item.ClassID,
item.GameID,
)
if err != nil {
return err
}
return nil
}
func (db PostgresDB) AddItemHistory(ctx context.Context, dbItemHistory domain.DBItemHistory) error {
query := `
INSERT INTO item_history (item_id, price, count, date)
VALUES ($1, $2, $3, $4)
`
_, err := db.conn.Exec(ctx, query,
dbItemHistory.ItemID,
dbItemHistory.Price,
dbItemHistory.Count,
dbItemHistory.Date,
)
if err != nil {
return err
}
return nil
}
func (db PostgresDB) GetItemByClassID(ctx context.Context, classID string) (*domain.SteamItem, error) {
query := `
SELECT
id,
name,
class_id
FROM item
WHERE class_id = $1
`
row := db.conn.QueryRow(ctx, query, classID)
var steamItem domain.SteamItem
err := row.Scan(
&steamItem.ID,
&steamItem.Name,
&steamItem.ClassID,
)
if err != nil {
return nil, err
}
return &steamItem, nil
}
func (db PostgresDB) GetItemPriceHistoryByID(ctx context.Context, itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error) {
query := `
SELECT
price,
count,
date
FROM item_history
WHERE item_id = $1
ORDER BY date DESC
LIMIT $2
OFFSET $3
`
var itemPriceHistory []domain.ItemPriceHistory
rows, err := db.conn.Query(ctx, query,
itemID,
limit,
offset,
)
if err != nil {
return nil, err
}
for rows.Next() {
var steamItemPriceHistory domain.ItemPriceHistory
err := rows.Scan(
&steamItemPriceHistory.Price,
&steamItemPriceHistory.Count,
&steamItemPriceHistory.Date,
)
if err != nil {
fmt.Println("error while iterating rows", err)
continue
}
itemPriceHistory = append(itemPriceHistory, steamItemPriceHistory)
}
return itemPriceHistory, nil
}
func (db PostgresDB) GetItemsWithLastPriceByName(ctx context.Context, name string) ([]ItemWithLastPrice, error) {
query := `
SELECT
class_id,
name,
price,
last_date
FROM item
JOIN (
SELECT
item_id,
MAX(price) AS price,
MAX(date) AS last_date
FROM item_history
GROUP BY item_id
) as last_prices
ON item.id = last_prices.item_id
WHERE LOWER(name) like $1
ORDER BY last_date DESC;
`
rows, err := db.conn.Query(ctx, query,
"%"+name+"%",
)
if err != nil {
return nil, err
}
var itemsWithLastPrice []ItemWithLastPrice
var itemWithLastPrice ItemWithLastPrice
for rows.Next() {
err := rows.Scan(
&itemWithLastPrice.ClassID,
&itemWithLastPrice.Name,
&itemWithLastPrice.Price,
&itemWithLastPrice.Date,
)
if err != nil {
fmt.Println("error while scaning row", err)
continue
}
itemsWithLastPrice = append(itemsWithLastPrice, itemWithLastPrice)
}
return itemsWithLastPrice, nil
}

View File

@@ -0,0 +1,221 @@
// Package repository for repositorie's
package repository
import (
"database/sql"
"fmt"
"log"
"steam_analyzer/internal/domain"
"time"
"github.com/google/uuid"
)
type SQLLiteDatabase struct {
db *sql.DB
}
func NewSQLLiteDatabase(db *sql.DB) *SQLLiteDatabase {
return &SQLLiteDatabase{
db: db,
}
}
func ConnectOrCreateDatabase() *sql.DB {
// Connect to (or create) the SQLite database
db, err := sql.Open("sqlite3", "./steam_items.db")
if err != nil {
log.Fatal(err)
}
return db
}
func (sqldb SQLLiteDatabase) InitDatabaseTables() error {
createTable := `
CREATE TABLE IF NOT EXISTS item (
id UUID PRIMARY KEY,
name TEXT,
class_id TEXT UNIQUE NOT NULL,
game_id INTEGER NOT NULL,
type_id INTEGER
);`
_, err := sqldb.db.Exec(createTable)
if err != nil {
return err
}
createTable = `
CREATE TABLE IF NOT EXISTS item_history (
item_id UUID REFERENCES item(id),
price REAL,
count INTEGER,
date DATETIME NOT NULL,
PRIMARY KEY (item_id, date)
);`
_, err = sqldb.db.Exec(createTable)
if err != nil {
return err
}
return nil
}
func (sqldb SQLLiteDatabase) AddItem(item domain.DBItem) error {
query := `
INSERT INTO item (id, name, class_id, game_id)
VALUES (?, ?, ?, ?)
`
_, err := sqldb.db.Exec(query,
item.ID,
item.Name,
item.ClassID,
item.GameID,
)
if err != nil {
return err
}
return nil
}
func (sqldb SQLLiteDatabase) AddItemHistory(dbItemHistory domain.DBItemHistory) error {
query := `
INSERT INTO item_history (item_id, price, count, date)
VALUES (?, ?, ?, ?)
`
_, err := sqldb.db.Exec(query,
dbItemHistory.ItemID,
dbItemHistory.Price,
dbItemHistory.Count,
dbItemHistory.Date,
)
if err != nil {
return err
}
return nil
}
func (sqldb SQLLiteDatabase) GetItemByClassID(classID string) (*domain.SteamItem, error) {
query := `
SELECT
id,
name,
class_id
FROM item
WHERE class_id = ?
`
row := sqldb.db.QueryRow(query, classID)
var steamItem domain.SteamItem
err := row.Scan(
&steamItem.ID,
&steamItem.Name,
&steamItem.ClassID,
)
if err != nil {
return nil, err
}
return &steamItem, nil
}
func (sqldb SQLLiteDatabase) GetItemPriceHistoryByID(itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error) {
query := `
SELECT
price,
count,
date
FROM item_history
WHERE item_id = ?
ORDER BY date DESC
LIMIT ?
OFFSET ?
`
var itemPriceHistory []domain.ItemPriceHistory
rows, err := sqldb.db.Query(query,
itemID,
limit,
offset,
)
if err != nil {
return nil, err
}
for rows.Next() {
var steamItemPriceHistory domain.ItemPriceHistory
err := rows.Scan(
&steamItemPriceHistory.Price,
&steamItemPriceHistory.Count,
&steamItemPriceHistory.Date,
)
if err != nil {
fmt.Println("error while iterating rows", err)
continue
}
itemPriceHistory = append(itemPriceHistory, steamItemPriceHistory)
}
return itemPriceHistory, nil
}
func (sqldb SQLLiteDatabase) GetItemsWithLastPriceByName(name string) ([]ItemWithLastPrice, error) {
query := `
SELECT
item.name,
price,
last_date
FROM item
JOIN (
SELECT
item_id,
price,
max(date) as last_date
FROM item_history
GROUP BY item_id
) as last_prices
ON item.id = last_prices.item_id
WHERE item.name like ?
ORDER BY last_date DESC
`
rows, err := sqldb.db.Query(query,
"%"+name+"%",
)
if err != nil {
return nil, err
}
var itemsWithLastPrice []ItemWithLastPrice
var itemWithLastPrice ItemWithLastPrice
var lastDateStr string
layout := "2006-01-02 15:04:05.999999999-07:00"
for rows.Next() {
err := rows.Scan(
&itemWithLastPrice.Name,
&itemWithLastPrice.Price,
&lastDateStr,
)
if err != nil {
fmt.Println("error while scaning row", err)
continue
}
lastDate, err := time.Parse(layout, lastDateStr)
if err != nil {
fmt.Println("error while parsing date", err)
continue
}
itemWithLastPrice.Date = lastDate
itemsWithLastPrice = append(itemsWithLastPrice, itemWithLastPrice)
}
return itemsWithLastPrice, nil
}

View File

@@ -0,0 +1,10 @@
package repository
import "time"
type ItemWithLastPrice struct {
ClassID string
Name string
Price float64
Date time.Time
}

129
internal/service/item.go Normal file
View File

@@ -0,0 +1,129 @@
// Package service for service's
package service
import (
"context"
"database/sql"
"errors"
"log/slog"
"math"
"steam_analyzer/internal/domain"
"steam_analyzer/internal/repository"
"steam_analyzer/internal/utils"
"steam_analyzer/internal/utils/slogutils"
"steam_analyzer/pkg/postgresql"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgconn"
)
type ItemRepository interface {
AddItem(ctx context.Context, item domain.DBItem) error
AddItemHistory(ctx context.Context, dbItemHistory domain.DBItemHistory) error
GetItemByClassID(ctx context.Context, classID string) (*domain.SteamItem, error)
GetItemPriceHistoryByID(ctx context.Context, itemID uuid.UUID, limit int, offset int) ([]domain.ItemPriceHistory, error)
GetItemsWithLastPriceByName(ctx context.Context, name string) ([]repository.ItemWithLastPrice, error)
}
type ItemService struct {
logger *slog.Logger
itemRepo ItemRepository
}
func NewItemService(logger *slog.Logger, itemRepo ItemRepository) *ItemService {
return &ItemService{
logger: logger,
itemRepo: itemRepo,
}
}
func (s ItemService) AddItemWithHistory(ctx context.Context, item domain.SteamItem) error {
searchedItem, err := s.itemRepo.GetItemByClassID(ctx, item.ClassID)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
s.logger.Info("Item is new, creating it")
default:
return err
}
}
if searchedItem != nil {
item.ID = searchedItem.ID
} else {
item.ID = uuid.New()
err = s.itemRepo.AddItem(ctx, item.ToDB())
if err != nil {
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == postgresql.ErrConstraintUnique {
s.logger.Error("Error while adding item", slog.String("error", "Item already exists"))
} else {
s.logger.Error("Error while adding item", slogutils.Error(err))
return err
}
}
}
err = s.itemRepo.AddItemHistory(ctx, item.ToDBHistory())
if err != nil {
s.logger.Error("Error while adding item history", slogutils.Error(sql.ErrNoRows))
return err
}
return nil
}
func (s ItemService) GetItemWithPriceHistoryByClassID(ctx context.Context, classID string, limit int, offset int) (*domain.SteamItem, error) {
steamItem, err := s.itemRepo.GetItemByClassID(ctx, classID)
if err != nil {
s.logger.Error("Error while getting item by class id", slogutils.Error(err))
return nil, err
}
itemPriceHistory, err := s.itemRepo.GetItemPriceHistoryByID(ctx, steamItem.ID, limit, offset)
if err != nil {
s.logger.Error("Error while getting item price history by id", slogutils.Error(err))
return nil, err
}
steamItem.History = itemPriceHistory
return steamItem, nil
}
func (s ItemService) GetAllItemsWithLastPrice(ctx context.Context, itemName string) ([]repository.ItemWithLastPrice, error) {
s.logger.Info("ItemService.GetAllItemsWithLastPrice", slog.String("item name is", itemName))
items, err := s.itemRepo.GetItemsWithLastPriceByName(ctx, itemName)
if err != nil {
s.logger.Error("Error while getting item with last price", slogutils.Error(err))
return nil, err
}
for i, item := range items {
items[i].Price = formatPrice(item.Price)
items[i].Date = formatDate(item.Date)
}
return items, nil
}
func formatPrice(price float64) float64 {
formattedPrice := price / 100 * utils.CURRENCY_RUB
formattedPrice = math.Round(formattedPrice*100) / 100
return formattedPrice
}
func formatDate(date time.Time) time.Time {
location, _ := time.LoadLocation("Europe/Moscow")
foramettedDate := date.In(location)
return foramettedDate
}

View File

@@ -0,0 +1,107 @@
package steamreq
import (
"fmt"
"math"
"net/http"
"steam_analyzer/internal/utils/jsonutils"
"time"
"golang.org/x/time/rate"
)
type Categories struct {
caseCat string
knife string
}
type Requester struct {
baseMarketURL string
categories Categories
limiter *rate.Limiter
}
func NewRequester(baseURL string, perTime time.Duration, count int) *Requester {
return &Requester{
baseMarketURL: baseURL,
categories: Categories{
caseCat: "category_730_Type[]=tag_CSGO_Type_WeaponCase",
knife: "category_730_Type[]=tag_CSGO_Type_Knife",
},
limiter: rate.NewLimiter(rate.Every(perTime), count),
}
}
func (r *Requester) GetItem(query string, itemType string) ([]SteamMarketItem, error) {
var steamMarketItems []SteamMarketItem
var pageCount int = 1
for i := 0; i < pageCount; i++ {
//if err := r.limiter.Wait(context.Background()); err != nil {
// fmt.Printf("Limiter error: %v\n", err)
// continue
//}
response, err := r.doRequest(query, itemType, i)
if err != nil {
fmt.Printf("Error while doing request. Query: %v. ItemType: %v. Start %v", query, itemType, i)
continue
}
steamMarketItems = append(steamMarketItems, response.Results...)
if i == 0 {
pageCount = int(math.Ceil(float64(response.TotalCount) / float64(response.Pagesize)))
}
}
return steamMarketItems, nil
}
func (r *Requester) doRequest(query string, itemType string, start int) (*Response, error) {
itemURL := r.createItemURL(query, itemType, start*10)
// fmt.Println(itemURL)
req, err := http.NewRequest("GET", itemURL, nil)
if err != nil {
return nil, fmt.Errorf("error while creating request %v", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error while making http request %v", err)
}
defer resp.Body.Close()
var steamResponse Response
err = jsonutils.FromJSON(resp.Body, &steamResponse)
if err != nil {
return nil, fmt.Errorf("error while getting item %v", err)
}
return &steamResponse, nil
}
func (r *Requester) createItemURL(item string, itemType string, start int) string {
marketItemsURL := r.baseMarketURL + `?query=%v&start=%v&count=%v&search_descriptions=0&sort_column=%v&sort_dir=%v&appid=730&norender=1`
count := 10
sortColumn := "price"
sortDir := "asc"
url := fmt.Sprintf(marketItemsURL,
item,
start,
count,
sortColumn,
sortDir,
)
switch itemType {
case "case":
url = url + "&" + r.categories.caseCat
case "knife":
url = url + "&" + r.categories.knife
}
return url
}

33
internal/steamreq/type.go Normal file
View File

@@ -0,0 +1,33 @@
package steamreq
type Response struct {
Start int `json:"start"`
Pagesize int `json:"pagesize"`
TotalCount int `json:"total_count"`
Results []SteamMarketItem `json:"results"`
}
type SteamMarketItem struct {
Name string `json:"name"`
HashName string `json:"hash_name"`
SellListings int `json:"sell_listings"`
SellPrice float64 `json:"sell_price"`
SellPriceText string `json:"sell_price_text"`
AppIcon string `json:"app_icon"`
AppName string `json:"app_name"`
AssetDescription struct {
Appid int `json:"appid"`
Classid string `json:"classid"`
Instanceid string `json:"instanceid"`
BackgroundColor string `json:"background_color"`
IconURL string `json:"icon_url"`
Tradable int `json:"tradable"`
Name string `json:"name"`
NameColor string `json:"name_color"`
Type string `json:"type"`
MarketName string `json:"market_name"`
MarketHashName string `json:"market_hash_name"`
Commodity int `json:"commodity"`
} `json:"asset_description"`
SalePriceText string `json:"sale_price_text"`
}

View File

@@ -0,0 +1,80 @@
package steamreq
import (
"context"
"log/slog"
"os"
"steam_analyzer/internal/domain"
"steam_analyzer/internal/utils/slogutils"
"strconv"
"time"
)
type IRequester interface {
GetItem(query string, itemType string) ([]SteamMarketItem, error)
}
type ItemService interface {
AddItemWithHistory(ctx context.Context, item domain.SteamItem) error
}
type SteamReqWorker struct {
logger *slog.Logger
requester IRequester
itemService ItemService
reqPer time.Duration
}
func NewSteamWorker(logger *slog.Logger, requester IRequester, itemService ItemService, reqPer time.Duration) *SteamReqWorker {
return &SteamReqWorker{
logger: logger,
requester: requester,
itemService: itemService,
reqPer: reqPer,
}
}
type SteamItems struct {
Query string
ItemType string
}
func (w SteamReqWorker) Start(ctx context.Context, items []SteamItems) {
for {
for _, item := range items {
items, err := w.requester.GetItem(item.Query, item.ItemType)
if err != nil {
w.logger.Error("Error while request item from steam", slogutils.Error(err))
os.Exit(1)
}
w.logger.Info("Succesfully get items", slog.String("items count is", strconv.Itoa(len(items))))
domainItems := CreateItemsFromRequest(items)
for _, domainItem := range domainItems {
err := w.itemService.AddItemWithHistory(ctx, domainItem)
if err != nil {
w.logger.Error("Error while adding item with history", slogutils.Error(err))
}
}
}
time.Sleep(w.reqPer)
}
}
func CreateItemsFromRequest(smi []SteamMarketItem) []domain.SteamItem {
steamItem := make([]domain.SteamItem, 0, len(smi))
for _, item := range smi {
steamItem = append(steamItem, domain.SteamItem{
Name: item.Name,
ClassID: item.AssetDescription.Classid,
GameID: item.AssetDescription.Appid,
Price: item.SellPrice,
Count: item.SellListings,
})
}
return steamItem
}

View File

@@ -0,0 +1,16 @@
package jsonutils
import (
"encoding/json"
"io"
)
func ToJSON(w io.Writer, v interface{}) error {
e := json.NewEncoder(w)
return e.Encode(v)
}
func FromJSON(r io.Reader, v interface{}) error {
d := json.NewDecoder(r)
return d.Decode(v)
}

View File

@@ -0,0 +1,10 @@
package slogutils
import "log/slog"
func Error(err error) slog.Attr {
return slog.Attr{
Key: "error",
Value: slog.StringValue(err.Error()),
}
}

3
internal/utils/utils.go Normal file
View File

@@ -0,0 +1,3 @@
package utils
var CURRENCY_RUB = 78.41