Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/orm/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (o *ormImpl) FindAllBuys(tradeType string) ([]model.TradeRecord, error) {
func (o *ormImpl) FindEarliestUnmatchedBuy(itemName, exterior string, paintSeed, paintIndex int, paintWear float64, beforeTime int64) (*model.TradeRecord, error) {
var buys []model.TradeRecord
err := o.db.Where(
"trade_type = 'buy' AND item_name = ? AND (exterior = ? OR exterior = '' OR ? = '') AND paint_seed = ? AND paint_index = ? AND paint_wear BETWEEN ? AND ? AND trade_at <= ? AND consumed_quantity < quantity",
"trade_type = 'buy' AND REPLACE(item_name, ' ', '') = REPLACE(?, ' ', '') AND (exterior = ? OR exterior = '' OR ? = '') AND paint_seed = ? AND paint_index = ? AND paint_wear BETWEEN ? AND ? AND trade_at <= ? AND consumed_quantity < quantity",
itemName, exterior, exterior, paintSeed, paintIndex, paintWear-0.0001, paintWear+0.0001, beforeTime,
).Order("trade_at ASC").Limit(1).Find(&buys).Error
if err != nil || len(buys) == 0 {
Expand Down
58 changes: 45 additions & 13 deletions pkg/platform/csqaq/csqaq.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ func New(apiToken string, log Logger) *Client {
client: httpclient.New(
httpclient.WithBaseURL(apiBase),
httpclient.WithName("csqaq"),
httpclient.WithNoRetry(),
),
log: log,
}
Expand Down Expand Up @@ -100,12 +99,30 @@ var exteriorENToCN = map[string]string{
}

// ResolveGoodsInfo queries csqaq's get_good_id API by item name and matches
// the result by exterior. Returns the csqaq goods ID and market_hash_name.
func (c *Client) ResolveGoodsInfo(ctx context.Context, itemName, exterior string) (int, string, error) {
// Try with full item name first.
search := itemName
c.log.Debug("csqaq: resolve goods search", "search", search)
id, mhn, _ := c.searchGoods(ctx, search, itemName, exterior)
// the result by exterior. marketHashName (if already known from source data)
// is used for disambiguation in matchGoods and as a fallback search term.
func (c *Client) ResolveGoodsInfo(ctx context.Context, itemName, exterior, marketHashName string) (int, string, error) {
// If we already know the canonical English name, use it directly.
// It's the most reliable search term and avoids wasting rate limit
// budget on Chinese name searches that often don't match.
if marketHashName != "" {
c.log.Debug("csqaq: resolve goods search by mhn", "marketHashName", marketHashName)
id, mhn, err := c.searchGoods(ctx, marketHashName, itemName, exterior, marketHashName)
if err != nil {
c.log.Warn("csqaq: mhn search failed", "marketHashName", marketHashName, "err", err)
}
if id != 0 {
c.log.Debug("csqaq: mhn matched", "goodID", id, "mhn", mhn)
return id, mhn, nil
}
}

// Try with full Chinese item name.
c.log.Debug("csqaq: resolve goods search", "search", itemName)
id, mhn, err := c.searchGoods(ctx, itemName, itemName, exterior, marketHashName)
if err != nil {
c.log.Warn("csqaq: search goods failed", "search", itemName, "err", err)
}
if id != 0 {
return id, mhn, nil
}
Expand All @@ -114,9 +131,12 @@ func (c *Client) ResolveGoodsInfo(ctx context.Context, itemName, exterior string
if exterior != "" {
if idx := strings.LastIndex(itemName, "|"); idx >= 0 {
skin := strings.TrimSpace(itemName[idx+1:])
search = skin + " (" + exterior + ")"
search := skin + " (" + exterior + ")"
c.log.Debug("csqaq: fallback search", "search", search)
id, mhn, _ = c.searchGoods(ctx, search, itemName, exterior)
id, mhn, err = c.searchGoods(ctx, search, itemName, exterior, marketHashName)
if err != nil {
c.log.Warn("csqaq: fallback search failed", "search", search, "err", err)
}
if id != 0 {
c.log.Debug("csqaq: fallback matched", "goodID", id, "mhn", mhn)
return id, mhn, nil
Expand All @@ -128,7 +148,7 @@ func (c *Client) ResolveGoodsInfo(ctx context.Context, itemName, exterior string
return 0, "", nil
}

func (c *Client) searchGoods(ctx context.Context, search, itemName, exterior string) (int, string, error) {
func (c *Client) searchGoods(ctx context.Context, search, itemName, exterior, marketHashName string) (int, string, error) {
body, err := json.Marshal(map[string]any{
"page_index": 1,
"page_size": 20,
Expand Down Expand Up @@ -158,7 +178,7 @@ func (c *Client) searchGoods(ctx context.Context, search, itemName, exterior str
return 0, "", fmt.Errorf("csqaq: get_good_id api error code=%d msg=%q", ar.Code, ar.Msg)
}

id, mhn := matchGoods(ar.Data.Data, itemName, exterior)
id, mhn := matchGoods(ar.Data.Data, itemName, exterior, marketHashName)
return id, mhn, nil
}

Expand All @@ -170,7 +190,7 @@ func normalizeName(s string) string {
return s
}

func matchGoods(data map[string]goodsInfo, itemName, exterior string) (int, string) {
func matchGoods(data map[string]goodsInfo, itemName, exterior, marketHashName string) (int, string) {
if len(data) == 0 {
return 0, ""
}
Expand All @@ -182,9 +202,21 @@ func matchGoods(data map[string]goodsInfo, itemName, exterior string) (int, stri
}
}

// Multiple results — try matching by known market_hash_name first.
// This handles cases where platforms use incompatible Chinese translations
// for the same skin (e.g. "消音版" vs "消音型"), since v.MarketHashName
// is the canonical English name and will match the known value.
if marketHashName != "" {
for _, v := range data {
if normalizeName(v.MarketHashName) == normalizeName(marketHashName) {
return v.ID, v.MarketHashName
}
}
}

normItem := normalizeName(itemName)

// Multiple results — try to disambiguate by exterior.
// Try to disambiguate by exterior in v.Name.
if exterior != "" {
cnExt := exterior
if en, ok := exteriorENToCN[exterior]; ok {
Expand Down
2 changes: 1 addition & 1 deletion pkg/platform/price_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ type PriceInfo struct {

type PriceProvider interface {
GetPrices(ctx context.Context, marketHashNames []string) ([]PriceInfo, error)
ResolveGoodsInfo(ctx context.Context, itemName, exterior string) (goodID int, marketHashName string, err error)
ResolveGoodsInfo(ctx context.Context, itemName, exterior, marketHashName string) (goodID int, resolvedMHN string, err error)
}
31 changes: 30 additions & 1 deletion pkg/service/market/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type MarketService struct {
ttlMin int
provider platform.PriceProvider
stopRefresh chan struct{}
startMu sync.Mutex
refreshMu sync.Mutex
refreshWg sync.WaitGroup
}
Expand Down Expand Up @@ -131,6 +132,8 @@ func (s *MarketService) GetAllPrices() ([]platform.PriceInfo, error) {
// StartAutoRefresh begins periodic full refresh of all market prices.
// Safe to call multiple times — waits for previous loop to finish before starting a new one.
func (s *MarketService) StartAutoRefresh(_ context.Context) {
s.startMu.Lock()
defer s.startMu.Unlock()
if s.stopRefresh != nil {
close(s.stopRefresh)
s.refreshWg.Wait()
Expand Down Expand Up @@ -277,7 +280,7 @@ func (s *MarketService) resolveMissingGoods(ctx context.Context) {
continue
}

goodID, mhn, err := s.provider.ResolveGoodsInfo(ctx, k.ItemName, k.Exterior)
goodID, mhn, err := s.provider.ResolveGoodsInfo(ctx, k.ItemName, k.Exterior, s.findMarketHashName(k))
if err != nil {
s.log.Warn("market: resolve goods failed", "itemName", k.ItemName, "exterior", k.Exterior, "err", err)
continue
Expand Down Expand Up @@ -316,6 +319,32 @@ func (s *MarketService) findExistingGoods(k struct {
return 0, ""
}

// findMarketHashName returns any known market_hash_name for an item+exterior pair,
// even if csqaq_goods_id hasn't been resolved yet. Useful as a canonical search term
// when the Chinese item_name doesn't match csqaq's naming.
func (s *MarketService) findMarketHashName(k struct {
ItemName string
Exterior string
}) string {
for _, table := range []string{"inventory", "trade_records"} {
var mhn string
row := s.db.Table(table).
Select("market_hash_name").
Where("item_name = ? AND exterior = ? AND market_hash_name != ''", k.ItemName, k.Exterior).
Row()
if row == nil {
continue
}
if err := row.Scan(&mhn); err != nil {
continue
}
if mhn != "" {
return mhn
}
}
return ""
}

func (s *MarketService) updateGoods(k struct {
ItemName string
Exterior string
Expand Down
Loading