From 7cd6699b8465e9bd69552f8b785835850aab97cd Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 15 Aug 2026 11:14:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(listing):=20=E5=85=AC=E5=85=B1=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E7=9A=AE=E8=82=A4=E4=B8=8EIP=E5=9C=B0=E5=8C=BA?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E6=94=B9=E4=B8=BAAND=E5=85=A8=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E6=96=B0=E5=A2=9E=E8=B7=A8=E5=88=86=E7=BB=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .../internal/modules/listing/public_filter.go | 30 +++- .../modules/listing/public_filter_test.go | 133 ++++++++++++++++++ .../listings/composables/useListingQuery.ts | 2 +- 4 files changed, 159 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 456796b..3e0d8ba 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,4 @@ deploy/caddy/show-dist/** deploy/caddy/conf.d/show.caddy logs - +.gocache diff --git a/backend/internal/modules/listing/public_filter.go b/backend/internal/modules/listing/public_filter.go index 8858ae8..26a714e 100644 --- a/backend/internal/modules/listing/public_filter.go +++ b/backend/internal/modules/listing/public_filter.go @@ -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 { diff --git a/backend/internal/modules/listing/public_filter_test.go b/backend/internal/modules/listing/public_filter_test.go index c94568e..7eb5145 100644 --- a/backend/internal/modules/listing/public_filter_test.go +++ b/backend/internal/modules/listing/public_filter_test.go @@ -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}, diff --git a/frontend/src/features/listings/composables/useListingQuery.ts b/frontend/src/features/listings/composables/useListingQuery.ts index 8226e63..d567298 100644 --- a/frontend/src/features/listings/composables/useListingQuery.ts +++ b/frontend/src/features/listings/composables/useListingQuery.ts @@ -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,