首页增加比例筛选并优化排序

This commit is contained in:
yml
2026-06-18 16:18:41 +08:00
parent a0b5ed2efb
commit b7d5dbd1d3
13 changed files with 153 additions and 4 deletions
+2
View File
@@ -104,6 +104,8 @@ type PublicListQuery struct {
SkinName []string SkinName []string
MinCoin *float64 MinCoin *float64
MaxCoin *float64 MaxCoin *float64
MinRatio *float64
MaxRatio *float64
MinPrice *float64 MinPrice *float64
MaxPrice *float64 MaxPrice *float64
MinDeposit *float64 MinDeposit *float64
@@ -94,6 +94,8 @@ func parsePublicListQuery(c *gin.Context) PublicListQuery {
SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))), SkinName: parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))),
MinCoin: parseOptionalFloat(c.Query("min_coin")), MinCoin: parseOptionalFloat(c.Query("min_coin")),
MaxCoin: parseOptionalFloat(c.Query("max_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")), MinPrice: parseOptionalFloat(c.Query("min_price")),
MaxPrice: parseOptionalFloat(c.Query("max_price")), MaxPrice: parseOptionalFloat(c.Query("max_price")),
MinDeposit: parseOptionalFloat(c.Query("min_deposit")), MinDeposit: parseOptionalFloat(c.Query("min_deposit")),
@@ -65,6 +65,9 @@ func matchesPublicQuery(item ListingDTO, query PublicListQuery) bool {
if !numberInRange(coinM, NumberRange{Min: query.MinCoin, Max: query.MaxCoin}) { if !numberInRange(coinM, NumberRange{Min: query.MinCoin, Max: query.MaxCoin}) {
return false 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}) { if !numberInRange(price, NumberRange{Min: query.MinPrice, Max: query.MaxPrice}) {
return false return false
} }
@@ -106,10 +109,12 @@ func sortPublicListings(items []ListingDTO, sortKey string) {
return aAmmo > bAmmo return aAmmo > bAmmo
} }
return a.HafCoinAmount > b.HafCoinAmount return a.HafCoinAmount > b.HafCoinAmount
case "published", "recommended", "comprehensive", "": case "published":
return publicRecentLess(a, b) return publicRecentLess(a, b)
case "recommended", "comprehensive", "":
return publicShuffleLess(a, b)
default: 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 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 { func matchesPublicZone(item ListingDTO, zone string) bool {
switch zone { switch zone {
case "", "all": case "", "all":
@@ -225,6 +243,22 @@ func coinMFromListing(item ListingDTO) float64 {
return float64(item.HafCoinAmount) / 1000000 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 { func readAssetString(summary map[string]any, key string) string {
if summary == nil { if summary == nil {
return "" return ""
@@ -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,
})
}
}
}
@@ -2,6 +2,7 @@ package listing
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"time" "time"
@@ -117,7 +118,10 @@ func canListPublicWithSQL(query PublicListQuery) bool {
if len(query.SkinGroup) > 0 || len(query.SkinName) > 0 || len(query.ResourceRanges) > 0 { if len(query.SkinGroup) > 0 || len(query.SkinName) > 0 || len(query.ResourceRanges) > 0 {
return false 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 return false
} }
if query.MinDeposit != nil || query.MaxDeposit != nil || query.MinTotal != nil || query.MaxTotal != nil { 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") return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC")
case "coinDesc": case "coinDesc":
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC") 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") 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) { func (r *Repository) publicZoneCountsCached(ctx context.Context) (map[string]int64, error) {
now := time.Now() now := time.Now()
r.publicZoneCountsMu.Lock() r.publicZoneCountsMu.Lock()
@@ -64,6 +64,8 @@ export interface PublicListingQuery {
skin_name?: string skin_name?: string
min_coin?: number min_coin?: number
max_coin?: number max_coin?: number
min_ratio?: number
max_ratio?: number
min_price?: number min_price?: number
max_price?: number max_price?: number
min_deposit?: number min_deposit?: number
@@ -6,6 +6,7 @@ import StringFilter from './StringFilter.vue'
import SkinFilter from './SkinFilter.vue' import SkinFilter from './SkinFilter.vue'
import { import {
coinRangeOptions, coinRangeOptions,
ratioRangeOptions,
moneyRangeOptions, moneyRangeOptions,
totalRangeOptions, totalRangeOptions,
fireLevelRangeOptions, fireLevelRangeOptions,
@@ -134,6 +135,17 @@ function closePopover() {
@close="closePopover" @close="closePopover"
/> />
<RangeFilter
label="比例"
:model-min="filters.minRatio"
:model-max="filters.maxRatio"
:options="ratioRangeOptions"
:popover-props="filterPopoverProps('ratio')"
@update:model-min="updateFilter('minRatio', $event)"
@update:model-max="updateFilter('maxRatio', $event)"
@close="closePopover"
/>
<RangeFilter <RangeFilter
label="租金" label="租金"
:model-min="filters.minPrice" :model-min="filters.minPrice"
@@ -6,6 +6,14 @@ export const coinRangeOptions = [
{ label: '500+', min: 500, max: undefined }, { label: '500+', min: 500, max: undefined },
] ]
export const ratioRangeOptions = [
{ label: '全部比例', min: undefined, max: undefined },
{ label: '0-20', min: 0, max: 20 },
{ label: '20-30', min: 20, max: 30 },
{ label: '30-40', min: 30, max: 40 },
{ label: '40+', min: 40, max: undefined },
]
export const moneyRangeOptions = [ export const moneyRangeOptions = [
{ label: '全部区间', min: undefined, max: undefined }, { label: '全部区间', min: undefined, max: undefined },
{ label: '0-50', min: 0, max: 50 }, { label: '0-50', min: 0, max: 50 },
@@ -16,6 +16,8 @@ export const desktopHomeFilterQueryKeys = [
'skin_name', 'skin_name',
'min_coin', 'min_coin',
'max_coin', 'max_coin',
'min_ratio',
'max_ratio',
'min_price', 'min_price',
'max_price', 'max_price',
'min_deposit', 'min_deposit',
@@ -9,6 +9,7 @@ export type FilterPopoverKey =
| 'load' | 'load'
| 'region' | 'region'
| 'coin' | 'coin'
| 'ratio'
| 'price' | 'price'
| 'deposit' | 'deposit'
| 'total' | 'total'
@@ -32,6 +33,8 @@ export interface HomeFilters {
skinName: string skinName: string
minCoin: number | undefined minCoin: number | undefined
maxCoin: number | undefined maxCoin: number | undefined
minRatio: number | undefined
maxRatio: number | undefined
minPrice: number | undefined minPrice: number | undefined
maxPrice: number | undefined maxPrice: number | undefined
minDeposit: number | undefined minDeposit: number | undefined
@@ -59,6 +62,8 @@ export function useHomeFilters(
skinName: '', skinName: '',
minCoin: undefined, minCoin: undefined,
maxCoin: undefined, maxCoin: undefined,
minRatio: undefined,
maxRatio: undefined,
minPrice: undefined, minPrice: undefined,
maxPrice: undefined, maxPrice: undefined,
minDeposit: undefined, minDeposit: undefined,
@@ -116,6 +121,8 @@ export function useHomeFilters(
filters.skinName = '' filters.skinName = ''
filters.minCoin = undefined filters.minCoin = undefined
filters.maxCoin = undefined filters.maxCoin = undefined
filters.minRatio = undefined
filters.maxRatio = undefined
filters.minPrice = undefined filters.minPrice = undefined
filters.maxPrice = undefined filters.maxPrice = undefined
filters.minDeposit = undefined filters.minDeposit = undefined
@@ -36,6 +36,8 @@ export function useListingQuery(
skin_name: filters.skinName, skin_name: filters.skinName,
min_coin: filters.minCoin, min_coin: filters.minCoin,
max_coin: filters.maxCoin, max_coin: filters.maxCoin,
min_ratio: filters.minRatio,
max_ratio: filters.maxRatio,
min_price: filters.minPrice, min_price: filters.minPrice,
max_price: filters.maxPrice, max_price: filters.maxPrice,
min_deposit: filters.minDeposit, min_deposit: filters.minDeposit,
@@ -180,6 +180,8 @@ function applyRouteQueryToHomeState() {
filters.skinName = readQueryString(query, 'skin_name') filters.skinName = readQueryString(query, 'skin_name')
filters.minCoin = readQueryNumber(query, 'min_coin') filters.minCoin = readQueryNumber(query, 'min_coin')
filters.maxCoin = readQueryNumber(query, 'max_coin') filters.maxCoin = readQueryNumber(query, 'max_coin')
filters.minRatio = readQueryNumber(query, 'min_ratio')
filters.maxRatio = readQueryNumber(query, 'max_ratio')
filters.minPrice = readQueryNumber(query, 'min_price') filters.minPrice = readQueryNumber(query, 'min_price')
filters.maxPrice = readQueryNumber(query, 'max_price') filters.maxPrice = readQueryNumber(query, 'max_price')
filters.minDeposit = readQueryNumber(query, 'min_deposit') filters.minDeposit = readQueryNumber(query, 'min_deposit')
@@ -224,6 +226,8 @@ function buildDesktopHomeQueryValues(): Record<string, HomeQueryValue> {
skin_name: filters.skinName, skin_name: filters.skinName,
min_coin: filters.minCoin, min_coin: filters.minCoin,
max_coin: filters.maxCoin, max_coin: filters.maxCoin,
min_ratio: filters.minRatio,
max_ratio: filters.maxRatio,
min_price: filters.minPrice, min_price: filters.minPrice,
max_price: filters.maxPrice, max_price: filters.maxPrice,
min_deposit: filters.minDeposit, min_deposit: filters.minDeposit,
@@ -111,6 +111,12 @@ const rangePresets: Record<string, Array<{ label: string; min: string; max: stri
{ label: '300-500', min: '300', max: '500' }, { label: '300-500', min: '300', max: '500' },
{ label: '500以上', min: '500', max: '' }, { label: '500以上', min: '500', max: '' },
], ],
ratio: [
{ label: '0-20', min: '0', max: '20' },
{ label: '20-30', min: '20', max: '30' },
{ label: '30-40', min: '30', max: '40' },
{ label: '40以上', min: '40', max: '' },
],
deposit: [ deposit: [
{ label: '0-50', min: '0', max: '50' }, { label: '0-50', min: '0', max: '50' },
{ label: '50-100', min: '50', max: '100' }, { label: '50-100', min: '50', max: '100' },
@@ -191,6 +197,14 @@ const filterSections = computed<FilterSection[]>(() => [
minPlaceholder: '最低', minPlaceholder: '最低',
maxPlaceholder: '最高', maxPlaceholder: '最高',
}, },
{
key: 'ratio',
title: '比例',
type: 'range',
unit: 'w/元',
minPlaceholder: '最低比例',
maxPlaceholder: '最高比例',
},
{ key: 'server', title: '区服', type: 'chips', options: serverFilterOptions.value }, { key: 'server', title: '区服', type: 'chips', options: serverFilterOptions.value },
{ key: 'region', title: '资产地区', type: 'chips', options: regionFilterOptions.value }, { key: 'region', title: '资产地区', type: 'chips', options: regionFilterOptions.value },
{ key: 'login', title: '上号方式', type: 'chips', options: loginMethodFilterOptions.value }, { key: 'login', title: '上号方式', type: 'chips', options: loginMethodFilterOptions.value },
@@ -436,6 +450,9 @@ function buildListingQuery(page: number): PublicListingQuery {
} else if (key === 'coin') { } else if (key === 'coin') {
query.min_coin = min query.min_coin = min
query.max_coin = max query.max_coin = max
} else if (key === 'ratio') {
query.min_ratio = min
query.max_ratio = max
} else if (key === 'secretKd') { } else if (key === 'secretKd') {
query.min_secret_kd = min query.min_secret_kd = min
query.max_secret_kd = max query.max_secret_kd = max