diff --git a/pkg/orm/trade.go b/pkg/orm/trade.go index 00c204c..27c2ebc 100644 --- a/pkg/orm/trade.go +++ b/pkg/orm/trade.go @@ -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 { diff --git a/pkg/platform/csqaq/csqaq.go b/pkg/platform/csqaq/csqaq.go index 50d435f..93fe038 100644 --- a/pkg/platform/csqaq/csqaq.go +++ b/pkg/platform/csqaq/csqaq.go @@ -44,7 +44,6 @@ func New(apiToken string, log Logger) *Client { client: httpclient.New( httpclient.WithBaseURL(apiBase), httpclient.WithName("csqaq"), - httpclient.WithNoRetry(), ), log: log, } @@ -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 } @@ -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 @@ -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, @@ -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 } @@ -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, "" } @@ -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 { diff --git a/pkg/platform/price_provider.go b/pkg/platform/price_provider.go index ed1b9aa..657cf60 100644 --- a/pkg/platform/price_provider.go +++ b/pkg/platform/price_provider.go @@ -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) } diff --git a/pkg/service/market/service.go b/pkg/service/market/service.go index f0501f0..d502ddb 100644 --- a/pkg/service/market/service.go +++ b/pkg/service/market/service.go @@ -22,6 +22,7 @@ type MarketService struct { ttlMin int provider platform.PriceProvider stopRefresh chan struct{} + startMu sync.Mutex refreshMu sync.Mutex refreshWg sync.WaitGroup } @@ -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() @@ -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 @@ -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