package listing import ( "context" "errors" "time" "hfb_jysh/backend/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( // CollectionKindHistory 浏览记录;CollectionKindFavorite 收藏。 CollectionKindHistory = "history" CollectionKindFavorite = "favorite" maxCollectionItems = 100 ) var ErrCollectionKindInvalid = errors.New("invalid collection kind") type collectionEntry struct { ListingID uint64 OccurredAt time.Time } // CollectionItem 返回给前端的足迹项:商品快照 + 发生时间。 type CollectionItem struct { Listing ListingDTO `json:"listing"` OccurredAt time.Time `json:"occurred_at"` } type CollectionCounts struct { History int64 `json:"history"` Favorite int64 `json:"favorite"` } type CollectionList struct { Items []CollectionItem `json:"items"` Counts CollectionCounts `json:"counts"` } func validCollectionKind(kind string) bool { return kind == CollectionKindHistory || kind == CollectionKindFavorite } // UpsertCollection 写入浏览或收藏记录;同一用户+商品+类型只更新时间。 func (r *Repository) UpsertCollection(ctx context.Context, userID, listingID uint64, kind string) error { now := time.Now() entry := model.ListingCollection{ UserID: userID, ListingID: listingID, Kind: kind, OccurredAt: now, } return r.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "listing_id"}, {Name: "kind"}}, DoUpdates: clause.Assignments(map[string]any{ "occurred_at": now, "updated_at": now, }), }).Create(&entry).Error } func (r *Repository) DeleteCollection(ctx context.Context, userID, listingID uint64, kind string) error { return r.db.WithContext(ctx). Where("user_id = ? AND listing_id = ? AND kind = ?", userID, listingID, kind). Delete(&model.ListingCollection{}).Error } func (r *Repository) ClearCollection(ctx context.Context, userID uint64, kind string) error { return r.db.WithContext(ctx). Where("user_id = ? AND kind = ?", userID, kind). Delete(&model.ListingCollection{}).Error } func (r *Repository) ListCollectionEntries(ctx context.Context, userID uint64, kind string, limit int) ([]collectionEntry, error) { if limit <= 0 || limit > maxCollectionItems { limit = maxCollectionItems } var rows []model.ListingCollection if err := r.db.WithContext(ctx). Select("listing_id, occurred_at"). Where("user_id = ? AND kind = ?", userID, kind). Order("occurred_at DESC, id DESC"). Limit(limit). Find(&rows).Error; err != nil { return nil, err } entries := make([]collectionEntry, 0, len(rows)) for _, row := range rows { entries = append(entries, collectionEntry{ListingID: row.ListingID, OccurredAt: row.OccurredAt}) } return entries, nil } func (r *Repository) CountCollections(ctx context.Context, userID uint64) (CollectionCounts, error) { var rows []struct { Kind string Total int64 } if err := r.db.WithContext(ctx). Model(&model.ListingCollection{}). Select("kind, COUNT(*) AS total"). Where("user_id = ?", userID). Group("kind"). Scan(&rows).Error; err != nil { return CollectionCounts{}, err } counts := CollectionCounts{} for _, row := range rows { switch row.Kind { case CollectionKindHistory: counts.History = row.Total case CollectionKindFavorite: counts.Favorite = row.Total } } return counts, nil } func (r *Repository) ListingExists(ctx context.Context, listingID uint64) (bool, error) { var count int64 if err := r.db.WithContext(ctx). Model(&model.RentalListing{}). Where("id = ?", listingID). Count(&count).Error; err != nil { return false, err } return count > 0, nil } // listingsByIDs 复用公开列表的联表与出图逻辑,保证足迹页展示与详情页一致。 func (r *Repository) listingsByIDs(ctx context.Context, ids []uint64) (map[uint64]ListingDTO, error) { result := make(map[uint64]ListingDTO, len(ids)) if len(ids) == 0 { return result, nil } var rows []listingRow if err := r.baseQuery(ctx).Where("l.id IN ?", ids).Scan(&rows).Error; err != nil { return nil, err } for _, item := range publicListings(rowsToDTO(rows)) { result[item.ID] = item } return result, nil } func (s *Service) RecordCollectionView(ctx context.Context, userID, listingID uint64) error { if s.repo == nil { return ErrDependencyUnavailable } exists, err := s.repo.ListingExists(ctx, listingID) if err != nil { return err } if !exists { return gorm.ErrRecordNotFound } return s.repo.UpsertCollection(ctx, userID, listingID, CollectionKindHistory) } func (s *Service) AddCollectionFavorite(ctx context.Context, userID, listingID uint64) error { if s.repo == nil { return ErrDependencyUnavailable } exists, err := s.repo.ListingExists(ctx, listingID) if err != nil { return err } if !exists { return gorm.ErrRecordNotFound } return s.repo.UpsertCollection(ctx, userID, listingID, CollectionKindFavorite) } func (s *Service) RemoveCollectionFavorite(ctx context.Context, userID, listingID uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.DeleteCollection(ctx, userID, listingID, CollectionKindFavorite) } func (s *Service) RemoveCollectionHistory(ctx context.Context, userID, listingID uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.DeleteCollection(ctx, userID, listingID, CollectionKindHistory) } func (s *Service) ClearCollectionHistory(ctx context.Context, userID uint64) error { if s.repo == nil { return ErrDependencyUnavailable } return s.repo.ClearCollection(ctx, userID, CollectionKindHistory) } func (s *Service) ListCollections(ctx context.Context, userID uint64, kind string) (*CollectionList, error) { if s.repo == nil { return nil, ErrDependencyUnavailable } if !validCollectionKind(kind) { return nil, ErrCollectionKindInvalid } entries, err := s.repo.ListCollectionEntries(ctx, userID, kind, maxCollectionItems) if err != nil { return nil, err } ids := make([]uint64, 0, len(entries)) for _, entry := range entries { ids = append(ids, entry.ListingID) } listings, err := s.repo.listingsByIDs(ctx, ids) if err != nil { return nil, err } items := make([]CollectionItem, 0, len(entries)) for _, entry := range entries { listing, ok := listings[entry.ListingID] if !ok { // 商品被物理删除时跳过,避免整页 500。 continue } items = append(items, CollectionItem{Listing: listing, OccurredAt: entry.OccurredAt}) } counts, err := s.repo.CountCollections(ctx, userID) if err != nil { return nil, err } return &CollectionList{Items: items, Counts: counts}, nil }