feat(listing): 公共筛选皮肤与IP地区多选改为AND全匹配,新增跨分组测试

This commit is contained in:
yml2213
2026-08-15 11:14:47 +08:00
parent aef36dc4f5
commit 7cd6699b84
4 changed files with 159 additions and 8 deletions
+1 -1
View File
@@ -39,4 +39,4 @@ deploy/caddy/show-dist/**
deploy/caddy/conf.d/show.caddy
logs
.gocache
@@ -28,7 +28,7 @@ func matchesPublicQuery(item ListingDTO, query PublicListQuery) bool {
if !matchesAny(query.Server, strings.TrimSpace(item.ServerRegion)) {
return false
}
if len(query.Region) > 0 && !intersects(query.Region, assetRegionsFromSummary(item.AssetSummary)) {
if len(query.Region) > 0 && !containsAll(query.Region, assetRegionsFromSummary(item.AssetSummary)) {
return false
}
if !matchesAny(query.LoginMethod, strings.TrimSpace(item.LoginPlatform)) {
@@ -47,11 +47,9 @@ func matchesPublicQuery(item ListingDTO, query PublicListQuery) bool {
return false
}
if len(query.SkinName) > 0 {
if len(query.SkinGroup) > 0 {
if !skinGroupsContainAny(item.AssetSummary, query.SkinGroup, query.SkinName) {
return false
}
} else if !intersects(query.SkinName, skinNamesFromSummary(item.AssetSummary)) {
// 皮肤与 IP 地区均为多值字段:多选时要求账号包含所有选中项(AND),
// 而不是包含任一即命中(OR)。
if !containsAll(query.SkinName, skinNamesFromSummary(item.AssetSummary)) {
return false
}
} else if len(query.SkinGroup) > 0 && !skinGroupsHaveAny(item.AssetSummary, query.SkinGroup) {
@@ -213,6 +211,25 @@ func matchesAny(options []string, value string) bool {
return false
}
// containsAll 要求所有选中项都出现在 values 中(AND 匹配),
// 用于多值字段(IP 地区、皮肤)的多选筛选。
func containsAll(options []string, values []string) bool {
if len(options) == 0 {
return true
}
valueSet := make(map[string]struct{}, len(values))
for _, value := range values {
valueSet[value] = struct{}{}
}
for _, option := range options {
if _, ok := valueSet[option]; !ok {
return false
}
}
return true
}
// intersects 用于管理端等场景:账号包含任一选中项即命中(OR)。
func intersects(options []string, values []string) bool {
if len(options) == 0 {
return true
@@ -294,6 +311,7 @@ func skinNamesFromSummary(summary map[string]any) []string {
return result
}
// skinGroupsContainAny 用于管理端等场景:任一选中分组包含任一选中皮肤即命中(OR)。
func skinGroupsContainAny(summary map[string]any, groupKeys []string, skinNames []string) bool {
groups := skinGroupsFromSummary(summary)
for _, groupKey := range groupKeys {
@@ -21,6 +21,139 @@ func TestFilterPublicListingsByRatio(t *testing.T) {
}
}
func TestFilterPublicListingsRegionMultiSelectRequiresAll(t *testing.T) {
items := []ListingDTO{
{
ID: 1,
AssetSummary: map[string]any{
"common_regions": []any{"北京", "上海"},
},
},
{
ID: 2,
AssetSummary: map[string]any{
"common_regions": []any{"北京", "广东"},
},
},
}
// 选中「北京+上海」:只有同时包含两个地区的账号才进入待选(AND)
filtered := filterPublicListings(items, PublicListQuery{
Region: []string{"北京", "上海"},
})
if len(filtered) != 1 || filtered[0].ID != 1 {
t.Fatalf("expected only listing 1 matching all regions, got %#v", filtered)
}
// 单选不受影响
filtered = filterPublicListings(items, PublicListQuery{
Region: []string{"北京"},
})
if len(filtered) != 2 {
t.Fatalf("expected 2 listings matching single region, got %#v", filtered)
}
}
func TestFilterPublicListingsSkinMultiSelectRequiresAll(t *testing.T) {
items := []ListingDTO{
{
ID: 1,
AssetSummary: map[string]any{
"skin_groups": map[string]any{
"operatorRed": []any{"凌霄成卫", "蚀金玫瑰"},
"melee": []any{"坠星者"},
},
},
},
{
ID: 2,
AssetSummary: map[string]any{
"skin_groups": map[string]any{
"operatorRed": []any{"凌霄成卫"},
},
},
},
}
// 选中「凌霄成卫+蚀金玫瑰」:账号必须同时拥有两个皮肤才进入待选(AND)
filtered := filterPublicListings(items, PublicListQuery{
SkinName: []string{"凌霄成卫", "蚀金玫瑰"},
})
if len(filtered) != 1 || filtered[0].ID != 1 {
t.Fatalf("expected only listing 1 matching all skins, got %#v", filtered)
}
// 单选不受影响
filtered = filterPublicListings(items, PublicListQuery{
SkinName: []string{"凌霄成卫"},
})
if len(filtered) != 2 {
t.Fatalf("expected 2 listings matching single skin, got %#v", filtered)
}
// 移动端带 skin_group 时同样要求全部皮肤命中
filtered = filterPublicListings(items, PublicListQuery{
SkinGroup: []string{"operatorRed"},
SkinName: []string{"凌霄成卫", "坠星者"},
})
if len(filtered) != 1 || filtered[0].ID != 1 {
t.Fatalf("expected only listing 1 matching all skins with group, got %#v", filtered)
}
}
func TestFilterPublicListingsSkinGroupsAllAnd(t *testing.T) {
// 刀皮/红皮/枪皮(以及金皮)跨组多选:账号必须同时拥有所有选中的皮肤(AND)
items := []ListingDTO{
{
ID: 1,
AssetSummary: map[string]any{
"skin_groups": map[string]any{
"melee": []any{"坠星者", "暗星"},
"operatorRed": []any{"凌霄成卫"},
"operatorGold": []any{"露娜-古墓丽影联动"},
"weapon": []any{"M7棱镜攻势S2"},
},
},
},
{
// 缺枪皮:只命中刀皮+红皮,不应进入待选
ID: 2,
AssetSummary: map[string]any{
"skin_groups": map[string]any{
"melee": []any{"坠星者"},
"operatorRed": []any{"凌霄成卫"},
},
},
},
{
// 缺红皮
ID: 3,
AssetSummary: map[string]any{
"skin_groups": map[string]any{
"melee": []any{"坠星者"},
"weapon": []any{"M7棱镜攻势S2"},
},
},
},
}
query := PublicListQuery{
SkinName: []string{"坠星者", "凌霄成卫", "M7棱镜攻势S2"},
}
filtered := filterPublicListings(items, query)
if len(filtered) != 1 || filtered[0].ID != 1 {
t.Fatalf("expected only listing 1 with all knife/red/weapon skins, got %#v", filtered)
}
// 同组内多选(刀皮选两个)同样要求全部拥有
filtered = filterPublicListings(items, PublicListQuery{
SkinName: []string{"坠星者", "暗星"},
})
if len(filtered) != 1 || filtered[0].ID != 1 {
t.Fatalf("expected only listing 1 with both knife skins, got %#v", filtered)
}
}
func TestSortPublicListingsDefaultUsesShuffleKey(t *testing.T) {
items := []ListingDTO{
{ID: 101},
@@ -36,7 +36,7 @@ export function useListingQuery(
insurance: joinCSV(filters.insurance),
stamina: joinCSV(filters.stamina),
load: joinCSV(filters.load),
// 多选皮肤名;后端 skin_name CSV 为 OR 匹配
// 多选皮肤名;后端 skin_name CSV 为 AND 匹配(账号需包含所有选中皮肤)
skin_name: joinCSV(filters.skinNames),
min_coin: filters.minCoin,
max_coin: filters.maxCoin,