diff --git a/backend/internal/modules/listing/admin_filter.go b/backend/internal/modules/listing/admin_filter.go new file mode 100644 index 0000000..7a6d065 --- /dev/null +++ b/backend/internal/modules/listing/admin_filter.go @@ -0,0 +1,45 @@ +package listing + +func hasAdminAssetFilters(query AdminListQuery) bool { + return len(query.Insurance) > 0 || + len(query.Stamina) > 0 || + len(query.Load) > 0 || + len(query.SkinGroup) > 0 || + len(query.SkinName) > 0 +} + +func filterAdminListings(items []ListingDTO, query AdminListQuery) []ListingDTO { + if !hasAdminAssetFilters(query) { + return items + } + filtered := make([]ListingDTO, 0, len(items)) + for _, item := range items { + if !matchesAdminAssetQuery(item, query) { + continue + } + filtered = append(filtered, item) + } + return filtered +} + +func matchesAdminAssetQuery(item ListingDTO, query AdminListQuery) bool { + if !matchesAny(query.Insurance, readAssetString(item.AssetSummary, "season_insurance")) { + return false + } + if !matchesAny(query.Stamina, readAssetString(item.AssetSummary, "stamina_level")) { + return false + } + if !matchesAny(query.Load, readAssetString(item.AssetSummary, "load_level")) { + return false + } + if len(query.SkinName) > 0 { + if len(query.SkinGroup) > 0 { + return skinGroupsContainAny(item.AssetSummary, query.SkinGroup, query.SkinName) + } + return intersects(query.SkinName, skinNamesFromSummary(item.AssetSummary)) + } + if len(query.SkinGroup) > 0 { + return skinGroupsHaveAny(item.AssetSummary, query.SkinGroup) + } + return true +} diff --git a/backend/internal/modules/listing/admin_filter_test.go b/backend/internal/modules/listing/admin_filter_test.go new file mode 100644 index 0000000..f9c6e19 --- /dev/null +++ b/backend/internal/modules/listing/admin_filter_test.go @@ -0,0 +1,94 @@ +package listing + +import "testing" + +func TestFilterAdminListingsByAssetLevels(t *testing.T) { + items := []ListingDTO{ + { + ID: 1, + AssetSummary: map[string]any{ + "season_insurance": "3*3", + "stamina_level": "7级", + "load_level": "7级", + }, + }, + { + ID: 2, + AssetSummary: map[string]any{ + "season_insurance": "2*3", + "stamina_level": "7级", + "load_level": "6级", + }, + }, + } + + filtered := filterAdminListings(items, AdminListQuery{ + Insurance: []string{"3*3"}, + Stamina: []string{"7级"}, + Load: []string{"7级"}, + }) + + if len(filtered) != 1 || filtered[0].ID != 1 { + t.Fatalf("expected only listing 1 in 9/7/7 asset filter, got %#v", filtered) + } +} + +func TestFilterAdminListingsBySkinGroupAndName(t *testing.T) { + items := []ListingDTO{ + { + ID: 1, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "operatorRed": []any{"凌霄成卫"}, + "weapon": []any{"K416命运"}, + }, + }, + }, + { + ID: 2, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "operatorGold": []any{"露娜-金牌射手"}, + }, + }, + }, + } + + filtered := filterAdminListings(items, AdminListQuery{ + SkinGroup: []string{"operatorRed"}, + SkinName: []string{"凌霄成卫"}, + }) + + if len(filtered) != 1 || filtered[0].ID != 1 { + t.Fatalf("expected only listing 1 by red skin name, got %#v", filtered) + } +} + +func TestFilterAdminListingsBySkinGroupOnly(t *testing.T) { + items := []ListingDTO{ + { + ID: 1, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "operatorRed": []any{"凌霄成卫"}, + }, + }, + }, + { + ID: 2, + AssetSummary: map[string]any{ + "skin_groups": map[string]any{ + "operatorGold": []any{"露娜-金牌射手"}, + }, + }, + }, + } + + filtered := filterAdminListings(items, AdminListQuery{ + SkinGroup: []string{"operatorGold"}, + }) + + if len(filtered) != 1 || filtered[0].ID != 2 { + t.Fatalf("expected only listing 2 by gold skin group, got %#v", filtered) + } +} diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 4648428..44eaaf6 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -76,6 +76,11 @@ type AdminListQuery struct { Keyword string Status string ReviewStatus string + Insurance []string + Stamina []string + Load []string + SkinGroup []string + SkinName []string Limit int Page int PageSize int diff --git a/backend/internal/modules/listing/handler_parse.go b/backend/internal/modules/listing/handler_parse.go index ef9bc9b..c21b34c 100644 --- a/backend/internal/modules/listing/handler_parse.go +++ b/backend/internal/modules/listing/handler_parse.go @@ -74,6 +74,11 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) { query.Keyword = strings.TrimSpace(c.Query("keyword")) query.Status = c.Query("status") query.ReviewStatus = c.Query("review_status") + query.Insurance = parseCSVQuery(c.Query("insurance")) + query.Stamina = parseCSVQuery(c.Query("stamina")) + query.Load = parseCSVQuery(c.Query("load")) + query.SkinGroup = parseCSVQuery(c.Query("skin_group")) + query.SkinName = parseCSVQuery(firstNonEmpty(c.Query("skin_name"), c.Query("skin"))) return query, true } diff --git a/backend/internal/modules/listing/query.go b/backend/internal/modules/listing/query.go index 2a35ad5..306ca1a 100644 --- a/backend/internal/modules/listing/query.go +++ b/backend/internal/modules/listing/query.go @@ -29,6 +29,37 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Admi pageSize = 100 } + if hasAdminAssetFilters(query) { + var rows []listingRow + err := r.applyAdminListFilters(r.baseQuery(ctx), query). + Order("l.id DESC"). + Scan(&rows).Error + if err != nil { + return nil, err + } + items := filterAdminListings(rowsToDTO(rows), query) + total := int64(len(items)) + start := (page - 1) * pageSize + if start < 0 { + start = 0 + } + if start >= len(items) { + items = []ListingDTO{} + } else { + end := start + pageSize + if end > len(items) { + end = len(items) + } + items = items[start:end] + } + return &AdminListResult{ + Items: items, + Total: total, + Page: page, + PageSize: pageSize, + }, nil + } + countDB := r.applyAdminListFilters(r.db.WithContext(ctx).Table("rental_listings AS l"), query) var total int64 if err := countDB.Count(&total).Error; err != nil { diff --git a/frontend/src/features/admin/views/AdminListingsView.vue b/frontend/src/features/admin/views/AdminListingsView.vue index 6f7dea2..62f813b 100644 --- a/frontend/src/features/admin/views/AdminListingsView.vue +++ b/frontend/src/features/admin/views/AdminListingsView.vue @@ -1,10 +1,12 @@