From b7d5dbd1d3b92f5126126552f63547980a433b86 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 18 Jun 2026 16:18:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=A2=9E=E5=8A=A0=E6=AF=94?= =?UTF-8?q?=E4=BE=8B=E7=AD=9B=E9=80=89=E5=B9=B6=E4=BC=98=E5=8C=96=E6=8E=92?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/listing/dto.go | 2 + .../internal/modules/listing/handler_parse.go | 2 + .../internal/modules/listing/public_filter.go | 38 ++++++++++++++- .../modules/listing/public_filter_test.go | 46 +++++++++++++++++++ .../internal/modules/listing/public_query.go | 15 +++++- .../src/features/listings/api/listings.ts | 2 + .../listings/components/HomeFilters.vue | 12 +++++ .../listings/composables/useFilterOptions.ts | 8 ++++ .../composables/useHomeFilterQuery.ts | 2 + .../listings/composables/useHomeFilters.ts | 7 +++ .../listings/composables/useListingQuery.ts | 2 + .../src/features/listings/views/HomeView.vue | 4 ++ .../listings/views/MobileHomeView.vue | 17 +++++++ 13 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 backend/internal/modules/listing/public_filter_test.go diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 2b460b1..a89ba67 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -104,6 +104,8 @@ type PublicListQuery struct { SkinName []string MinCoin *float64 MaxCoin *float64 + MinRatio *float64 + MaxRatio *float64 MinPrice *float64 MaxPrice *float64 MinDeposit *float64 diff --git a/backend/internal/modules/listing/handler_parse.go b/backend/internal/modules/listing/handler_parse.go index 853baf3..f006378 100644 --- a/backend/internal/modules/listing/handler_parse.go +++ b/backend/internal/modules/listing/handler_parse.go @@ -94,6 +94,8 @@ func parsePublicListQuery(c *gin.Context) PublicListQuery { SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))), MinCoin: parseOptionalFloat(c.Query("min_coin")), MaxCoin: parseOptionalFloat(c.Query("max_coin")), + MinRatio: parseOptionalFloat(c.Query("min_ratio")), + MaxRatio: parseOptionalFloat(c.Query("max_ratio")), MinPrice: parseOptionalFloat(c.Query("min_price")), MaxPrice: parseOptionalFloat(c.Query("max_price")), MinDeposit: parseOptionalFloat(c.Query("min_deposit")), diff --git a/backend/internal/modules/listing/public_filter.go b/backend/internal/modules/listing/public_filter.go index 96e3db6..9b3648f 100644 --- a/backend/internal/modules/listing/public_filter.go +++ b/backend/internal/modules/listing/public_filter.go @@ -65,6 +65,9 @@ func matchesPublicQuery(item ListingDTO, query PublicListQuery) bool { if !numberInRange(coinM, NumberRange{Min: query.MinCoin, Max: query.MaxCoin}) { return false } + if !numberInRange(ratioFromListing(item), NumberRange{Min: query.MinRatio, Max: query.MaxRatio}) { + return false + } if !numberInRange(price, NumberRange{Min: query.MinPrice, Max: query.MaxPrice}) { return false } @@ -106,10 +109,12 @@ func sortPublicListings(items []ListingDTO, sortKey string) { return aAmmo > bAmmo } return a.HafCoinAmount > b.HafCoinAmount - case "published", "recommended", "comprehensive", "": + case "published": return publicRecentLess(a, b) + case "recommended", "comprehensive", "": + return publicShuffleLess(a, b) default: - return publicRecentLess(a, b) + return publicShuffleLess(a, b) } }) } @@ -129,6 +134,19 @@ func publicRecentLess(a ListingDTO, b ListingDTO) bool { return a.ID > b.ID } +func publicShuffleLess(a ListingDTO, b ListingDTO) bool { + aKey := publicShuffleKey(a.ID) + bKey := publicShuffleKey(b.ID) + if aKey != bKey { + return aKey < bKey + } + return a.ID < b.ID +} + +func publicShuffleKey(id uint64) uint64 { + return (id*1103515245 + uint64(publicShuffleSeed())) % 2147483647 +} + func matchesPublicZone(item ListingDTO, zone string) bool { switch zone { case "", "all": @@ -225,6 +243,22 @@ func coinMFromListing(item ListingDTO) float64 { return float64(item.HafCoinAmount) / 1000000 } +func coinWanFromListing(item ListingDTO) float64 { + return float64(item.HafCoinAmount) / 10000 +} + +func ratioFromListing(item ListingDTO) float64 { + ratio := readSummaryNumber(item.AssetSummary["publish_ratio"]) + if ratio > 0 { + return ratio + } + price := float64(item.PriceCent) / 100 + if price <= 0 { + return 0 + } + return coinWanFromListing(item) / price +} + func readAssetString(summary map[string]any, key string) string { if summary == nil { return "" diff --git a/backend/internal/modules/listing/public_filter_test.go b/backend/internal/modules/listing/public_filter_test.go new file mode 100644 index 0000000..f0c341d --- /dev/null +++ b/backend/internal/modules/listing/public_filter_test.go @@ -0,0 +1,46 @@ +package listing + +import "testing" + +func TestFilterPublicListingsByRatio(t *testing.T) { + minRatio := 20.0 + maxRatio := 30.0 + items := []ListingDTO{ + {ID: 1, HafCoinAmount: 100_000_000, PriceCent: 10_000, AssetSummary: map[string]any{"publish_ratio": 10.0}}, + {ID: 2, HafCoinAmount: 100_000_000, PriceCent: 4_000, AssetSummary: map[string]any{"publish_ratio": 25.0}}, + {ID: 3, HafCoinAmount: 100_000_000, PriceCent: 2_500, AssetSummary: map[string]any{"publish_ratio": 40.0}}, + } + + filtered := filterPublicListings(items, PublicListQuery{ + MinRatio: &minRatio, + MaxRatio: &maxRatio, + }) + + if len(filtered) != 1 || filtered[0].ID != 2 { + t.Fatalf("expected only listing 2 in ratio range, got %#v", filtered) + } +} + +func TestSortPublicListingsDefaultUsesShuffleKey(t *testing.T) { + items := []ListingDTO{ + {ID: 101}, + {ID: 102}, + {ID: 103}, + {ID: 104}, + } + + sortPublicListings(items, "recommended") + + for index := 1; index < len(items); index++ { + previous := publicShuffleKey(items[index-1].ID) + current := publicShuffleKey(items[index].ID) + if previous > current { + t.Fatalf("expected shuffled order by key, got ids=%v", []uint64{ + items[0].ID, + items[1].ID, + items[2].ID, + items[3].ID, + }) + } + } +} diff --git a/backend/internal/modules/listing/public_query.go b/backend/internal/modules/listing/public_query.go index 5ed02d3..9741786 100644 --- a/backend/internal/modules/listing/public_query.go +++ b/backend/internal/modules/listing/public_query.go @@ -2,6 +2,7 @@ package listing import ( "context" + "fmt" "strings" "time" @@ -117,7 +118,10 @@ func canListPublicWithSQL(query PublicListQuery) bool { if len(query.SkinGroup) > 0 || len(query.SkinName) > 0 || len(query.ResourceRanges) > 0 { return false } - if query.MinCoin != nil || query.MaxCoin != nil || query.MinPrice != nil || query.MaxPrice != nil { + if query.MinCoin != nil || query.MaxCoin != nil || query.MinRatio != nil || query.MaxRatio != nil { + return false + } + if query.MinPrice != nil || query.MaxPrice != nil { return false } if query.MinDeposit != nil || query.MaxDeposit != nil || query.MinTotal != nil || query.MaxTotal != nil { @@ -142,11 +146,18 @@ func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB { return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC") case "coinDesc": return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC") - default: + case "published": return db.Order("l.published_at DESC, l.id DESC") + default: + return db.Order(fmt.Sprintf("((l.id * 1103515245 + %d) %% 2147483647) ASC, l.id ASC", publicShuffleSeed())) } } +func publicShuffleSeed() int64 { + now := time.Now().UTC() + return int64(now.Year()*366 + now.YearDay()) +} + func (r *Repository) publicZoneCountsCached(ctx context.Context) (map[string]int64, error) { now := time.Now() r.publicZoneCountsMu.Lock() diff --git a/frontend/src/features/listings/api/listings.ts b/frontend/src/features/listings/api/listings.ts index 1784a91..d00793c 100644 --- a/frontend/src/features/listings/api/listings.ts +++ b/frontend/src/features/listings/api/listings.ts @@ -64,6 +64,8 @@ export interface PublicListingQuery { skin_name?: string min_coin?: number max_coin?: number + min_ratio?: number + max_ratio?: number min_price?: number max_price?: number min_deposit?: number diff --git a/frontend/src/features/listings/components/HomeFilters.vue b/frontend/src/features/listings/components/HomeFilters.vue index 0b8b090..31570d8 100644 --- a/frontend/src/features/listings/components/HomeFilters.vue +++ b/frontend/src/features/listings/components/HomeFilters.vue @@ -6,6 +6,7 @@ import StringFilter from './StringFilter.vue' import SkinFilter from './SkinFilter.vue' import { coinRangeOptions, + ratioRangeOptions, moneyRangeOptions, totalRangeOptions, fireLevelRangeOptions, @@ -134,6 +135,17 @@ function closePopover() { @close="closePopover" /> + + { skin_name: filters.skinName, min_coin: filters.minCoin, max_coin: filters.maxCoin, + min_ratio: filters.minRatio, + max_ratio: filters.maxRatio, min_price: filters.minPrice, max_price: filters.maxPrice, min_deposit: filters.minDeposit, diff --git a/frontend/src/features/listings/views/MobileHomeView.vue b/frontend/src/features/listings/views/MobileHomeView.vue index 7e83ae5..d8608a3 100644 --- a/frontend/src/features/listings/views/MobileHomeView.vue +++ b/frontend/src/features/listings/views/MobileHomeView.vue @@ -111,6 +111,12 @@ const rangePresets: Record(() => [ minPlaceholder: '最低', maxPlaceholder: '最高', }, + { + key: 'ratio', + title: '比例', + type: 'range', + unit: 'w/元', + minPlaceholder: '最低比例', + maxPlaceholder: '最高比例', + }, { key: 'server', title: '区服', type: 'chips', options: serverFilterOptions.value }, { key: 'region', title: '资产地区', type: 'chips', options: regionFilterOptions.value }, { key: 'login', title: '上号方式', type: 'chips', options: loginMethodFilterOptions.value }, @@ -436,6 +450,9 @@ function buildListingQuery(page: number): PublicListingQuery { } else if (key === 'coin') { query.min_coin = min query.max_coin = max + } else if (key === 'ratio') { + query.min_ratio = min + query.max_ratio = max } else if (key === 'secretKd') { query.min_secret_kd = min query.max_secret_kd = max