From 02598db333db56d8a5358aaa25e7772c61c0fc3a Mon Sep 17 00:00:00 2001 From: zerosaturation Date: Wed, 20 May 2026 12:36:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=82=B9=E8=B5=9E=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pkg/models/asset.go | 15 +- .../repository/asset_like_repository.go | 28 +- .../repository/asset_like_repository_test.go | 329 ------------------ .../repository/asset_repository.go | 27 ++ .../service/asset_like_service.go | 64 ++-- .../assetService/service/asset_service.go | 26 +- .../galleryService/service/cleanup_worker.go | 13 +- .../repository/social_repository.go | 18 +- 8 files changed, 122 insertions(+), 398 deletions(-) delete mode 100644 backend/services/assetService/repository/asset_like_repository_test.go diff --git a/backend/pkg/models/asset.go b/backend/pkg/models/asset.go index 46cc9e6..87626c2 100644 --- a/backend/pkg/models/asset.go +++ b/backend/pkg/models/asset.go @@ -191,14 +191,17 @@ const ( // ========== 点赞记录表模型 ========== // AssetLike 点赞记录表模型 +// 唯一约束:(asset_id, user_id, exhibition_id) - 每次展览每个用户只能点赞一次 +// 注意:exhibition_id 不设置外键约束,因为迁移时现有数据没有有效的 exhibition_id type AssetLike struct { - ID int64 `gorm:"primaryKey;autoIncrement;column:id"` - AssetID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_asset;index:idx_asset_likes_asset;column:asset_id"` - UserID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_asset;index:idx_asset_likes_user_star;column:user_id"` - StarID int64 `gorm:"not null;index:idx_asset_likes_user_star;column:star_id"` // 用于数据隔离和查询优化 - CreatedAt int64 `gorm:"not null;index:idx_asset_likes_user_star,sort:desc;index:idx_asset_likes_asset,sort:desc;column:created_at"` + ID int64 `gorm:"primaryKey;autoIncrement;column:id"` + AssetID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_exhibition;index:idx_asset_likes_asset;column:asset_id"` + UserID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_exhibition;index:idx_asset_likes_user_star;column:user_id"` + StarID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_exhibition;index:idx_asset_likes_user_star;column:star_id"` // 用于数据隔离和查询优化 + ExhibitionID int64 `gorm:"not null;uniqueIndex:uk_asset_likes_user_exhibition;index:idx_asset_likes_exhibition;column:exhibition_id"` // 关联展览,同一展览只能点赞一次 + CreatedAt int64 `gorm:"not null;index:idx_asset_likes_user_star,sort:desc;index:idx_asset_likes_asset,sort:desc;column:created_at"` - // 关联关系 + // 关联关系(不设置外键约束,避免迁移问题) Asset Asset `gorm:"foreignKey:AssetID;references:ID;constraint:OnDelete:CASCADE"` User User `gorm:"foreignKey:UserID;references:ID;constraint:OnDelete:CASCADE"` Star Star `gorm:"foreignKey:StarID;references:StarID;constraint:OnDelete:CASCADE"` diff --git a/backend/services/assetService/repository/asset_like_repository.go b/backend/services/assetService/repository/asset_like_repository.go index b39d266..50d8cae 100644 --- a/backend/services/assetService/repository/asset_like_repository.go +++ b/backend/services/assetService/repository/asset_like_repository.go @@ -13,10 +13,10 @@ type AssetLikeRepository interface { Create(like *models.AssetLike) error // Delete 删除点赞记录 - Delete(assetID, userID, starID int64) error + Delete(assetID, userID, starID, exhibitionID int64) error - // Exists 检查点赞记录是否存在 - Exists(assetID, userID, starID int64) (bool, error) + // Exists 检查点赞记录是否存在(按 asset_id, user_id, exhibition_id) + Exists(assetID, userID, starID, exhibitionID int64) (bool, error) // GetByAsset 获取资产的点赞记录列表(分页) GetByAsset(assetID int64, limit, offset int) ([]*models.AssetLike, error) @@ -61,8 +61,8 @@ func (r *assetLikeRepository) Create(like *models.AssetLike) error { return errors.New("star_id must be greater than 0") } - // 检查是否已存在 - exists, err := r.Exists(like.AssetID, like.UserID, like.StarID) + // 检查是否已存在(同一展览同用户只能点赞一次) + exists, err := r.Exists(like.AssetID, like.UserID, like.StarID, like.ExhibitionID) if err != nil { return err } @@ -78,7 +78,7 @@ func (r *assetLikeRepository) Create(like *models.AssetLike) error { } // Delete 删除点赞记录 -func (r *assetLikeRepository) Delete(assetID, userID, starID int64) error { +func (r *assetLikeRepository) Delete(assetID, userID, starID, exhibitionID int64) error { if assetID <= 0 { return errors.New("asset_id must be greater than 0") } @@ -91,7 +91,11 @@ func (r *assetLikeRepository) Delete(assetID, userID, starID int64) error { return errors.New("star_id must be greater than 0") } - result := r.db.Where("asset_id = ? AND user_id = ? AND star_id = ?", assetID, userID, starID). + if exhibitionID <= 0 { + return errors.New("exhibition_id must be greater than 0") + } + + result := r.db.Where("asset_id = ? AND user_id = ? AND star_id = ? AND exhibition_id = ?", assetID, userID, starID, exhibitionID). Delete(&models.AssetLike{}) if result.Error != nil { @@ -105,8 +109,8 @@ func (r *assetLikeRepository) Delete(assetID, userID, starID int64) error { return nil } -// Exists 检查点赞记录是否存在 -func (r *assetLikeRepository) Exists(assetID, userID, starID int64) (bool, error) { +// Exists 检查点赞记录是否存在(按 asset_id, user_id, exhibition_id) +func (r *assetLikeRepository) Exists(assetID, userID, starID, exhibitionID int64) (bool, error) { if assetID <= 0 { return false, errors.New("asset_id must be greater than 0") } @@ -119,9 +123,13 @@ func (r *assetLikeRepository) Exists(assetID, userID, starID int64) (bool, error return false, errors.New("star_id must be greater than 0") } + if exhibitionID <= 0 { + return false, errors.New("exhibition_id must be greater than 0") + } + var count int64 err := r.db.Model(&models.AssetLike{}). - Where("asset_id = ? AND user_id = ? AND star_id = ?", assetID, userID, starID). + Where("asset_id = ? AND user_id = ? AND star_id = ? AND exhibition_id = ?", assetID, userID, starID, exhibitionID). Count(&count).Error if err != nil { diff --git a/backend/services/assetService/repository/asset_like_repository_test.go b/backend/services/assetService/repository/asset_like_repository_test.go deleted file mode 100644 index f756fe6..0000000 --- a/backend/services/assetService/repository/asset_like_repository_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package repository - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/topfans/backend/pkg/models" -) - -// TestAssetLikeRepository_Create 测试创建点赞记录 -func TestAssetLikeRepository_Create(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_create") - user := createTestUser(t, db, "19900200001") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - asset := createTestAsset(t, db, user.ID, star.StarID, "测试资产") - - // 测试创建点赞 - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - - err := repo.Create(like) - assert.NoError(t, err) - assert.NotZero(t, like.ID) - assert.NotZero(t, like.CreatedAt) - - // 测试重复点赞 - duplicateLike := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err = repo.Create(duplicateLike) - assert.Error(t, err) - assert.Contains(t, err.Error(), "already liked") -} - -// TestAssetLikeRepository_Delete 测试删除点赞记录 -func TestAssetLikeRepository_Delete(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_delete") - user := createTestUser(t, db, "19900200002") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - asset := createTestAsset(t, db, user.ID, star.StarID, "测试资产") - - // 先创建点赞 - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err := repo.Create(like) - assert.NoError(t, err) - - // 删除点赞 - err = repo.Delete(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - - // 验证已删除 - exists, err := repo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.False(t, exists) - - // 测试删除不存在的记录 - err = repo.Delete(asset.ID, user.ID, star.StarID) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not found") -} - -// TestAssetLikeRepository_Exists 测试检查点赞是否存在 -func TestAssetLikeRepository_Exists(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_exists") - user := createTestUser(t, db, "19900200003") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - asset := createTestAsset(t, db, user.ID, star.StarID, "测试资产") - - // 初始状态不存在 - exists, err := repo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.False(t, exists) - - // 创建点赞后存在 - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err = repo.Create(like) - assert.NoError(t, err) - - exists, err = repo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.True(t, exists) -} - -// TestAssetLikeRepository_GetByAsset 测试获取资产的点赞列表 -func TestAssetLikeRepository_GetByAsset(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_by_asset") - // 创建资产的所有者 - assetOwner := createTestUser(t, db, "19900200004") - createTestFanProfile(t, db, assetOwner.ID, star.StarID, "资产所有者") - asset := createTestAsset(t, db, assetOwner.ID, star.StarID, "测试资产") - - // 创建多个用户点赞 - for i := 1; i <= 5; i++ { - mobile := "1990020000" + string(rune('4'+i)) - user := createTestUser(t, db, mobile) - createTestFanProfile(t, db, user.ID, star.StarID, fmt.Sprintf("用户%d", i)) - - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err := repo.Create(like) - assert.NoError(t, err) - } - - // 获取点赞列表 - likes, err := repo.GetByAsset(asset.ID, 3, 0) - assert.NoError(t, err) - assert.Len(t, likes, 3) - - // 获取第二页 - likes, err = repo.GetByAsset(asset.ID, 3, 3) - assert.NoError(t, err) - assert.Len(t, likes, 2) -} - -// TestAssetLikeRepository_GetByUser 测试获取用户的点赞列表 -func TestAssetLikeRepository_GetByUser(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_by_user") - user := createTestUser(t, db, "19900200009") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - - // 创建多个资产并点赞 - for i := 0; i < 5; i++ { - asset := createTestAsset(t, db, user.ID, star.StarID, "资产") - - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err := repo.Create(like) - assert.NoError(t, err) - } - - // 获取点赞列表 - likes, err := repo.GetByUser(user.ID, star.StarID, 3, 0) - assert.NoError(t, err) - assert.Len(t, likes, 3) - - // 获取第二页 - likes, err = repo.GetByUser(user.ID, star.StarID, 3, 3) - assert.NoError(t, err) - assert.Len(t, likes, 2) -} - -// TestAssetLikeRepository_CountByAsset 测试统计资产的点赞数 -func TestAssetLikeRepository_CountByAsset(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_count_asset") - // 创建资产的所有者 - assetOwner := createTestUser(t, db, "19900200010") - createTestFanProfile(t, db, assetOwner.ID, star.StarID, "资产所有者") - asset := createTestAsset(t, db, assetOwner.ID, star.StarID, "测试资产") - - // 初始点赞数为0 - count, err := repo.CountByAsset(asset.ID) - assert.NoError(t, err) - assert.Equal(t, int64(0), count) - - // 创建3个点赞 - for i := 1; i <= 3; i++ { - mobile := "1990020001" + string(rune('0'+i)) - user := createTestUser(t, db, mobile) - createTestFanProfile(t, db, user.ID, star.StarID, fmt.Sprintf("用户%d", i)) - - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err := repo.Create(like) - assert.NoError(t, err) - } - - // 验证点赞数 - count, err = repo.CountByAsset(asset.ID) - assert.NoError(t, err) - assert.Equal(t, int64(3), count) -} - -// TestAssetLikeRepository_CountByUser 测试统计用户的点赞数 -func TestAssetLikeRepository_CountByUser(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - repo := NewAssetLikeRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_count_user") - user := createTestUser(t, db, "19900200014") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - - // 初始点赞数为0 - count, err := repo.CountByUser(user.ID, star.StarID) - assert.NoError(t, err) - assert.Equal(t, int64(0), count) - - // 创建3个点赞 - for i := 0; i < 3; i++ { - asset := createTestAsset(t, db, user.ID, star.StarID, "资产") - - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err := repo.Create(like) - assert.NoError(t, err) - } - - // 验证点赞数 - count, err = repo.CountByUser(user.ID, star.StarID) - assert.NoError(t, err) - assert.Equal(t, int64(3), count) -} - -// TestAssetLikeRepository_LikeUnlikeFlow 测试点赞-取消点赞流程 -func TestAssetLikeRepository_LikeUnlikeFlow(t *testing.T) { - db := setupTestDB(t) - defer cleanupTestDB(t, db) - - assetLikeRepo := NewAssetLikeRepository(db) - assetRepo := NewAssetRepository(db) - - // 创建测试数据 - star := createTestStar(t, db, "test_like_flow") - user := createTestUser(t, db, "19900200015") - createTestFanProfile(t, db, user.ID, star.StarID, "测试用户") - asset := createTestAsset(t, db, user.ID, star.StarID, "测试资产") - - // 1. 初始状态 - exists, err := assetLikeRepo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.False(t, exists) - - assetData, err := assetRepo.GetByID(asset.ID) - assert.NoError(t, err) - assert.Equal(t, int32(0), assetData.LikeCount) - - // 2. 点赞 - like := &models.AssetLike{ - AssetID: asset.ID, - UserID: user.ID, - StarID: star.StarID, - } - err = assetLikeRepo.Create(like) - assert.NoError(t, err) - - // 增加资产点赞数 - err = assetRepo.IncrementLikeCount(asset.ID) - assert.NoError(t, err) - - // 验证点赞状态 - exists, err = assetLikeRepo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.True(t, exists) - - assetData, err = assetRepo.GetByID(asset.ID) - assert.NoError(t, err) - assert.Equal(t, int32(1), assetData.LikeCount) - - // 3. 取消点赞 - err = assetLikeRepo.Delete(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - - // 减少资产点赞数 - err = assetRepo.DecrementLikeCount(asset.ID) - assert.NoError(t, err) - - // 验证取消点赞状态 - exists, err = assetLikeRepo.Exists(asset.ID, user.ID, star.StarID) - assert.NoError(t, err) - assert.False(t, exists) - - assetData, err = assetRepo.GetByID(asset.ID) - assert.NoError(t, err) - assert.Equal(t, int32(0), assetData.LikeCount) -} diff --git a/backend/services/assetService/repository/asset_repository.go b/backend/services/assetService/repository/asset_repository.go index 2e027c4..1cd4c62 100644 --- a/backend/services/assetService/repository/asset_repository.go +++ b/backend/services/assetService/repository/asset_repository.go @@ -54,6 +54,9 @@ type AssetRepository interface { // IsExhibiting 检查资产是否正在展出中 IsExhibiting(assetID int64) (bool, error) + // GetExhibitingID 获取正在展出的展览ID(如果正在展出),返回0表示未展出 + GetExhibitingID(assetID int64) (int64, error) + // GetExhibitionStartTime 获取资产展出开始时间(如果正在展出) GetExhibitionStartTime(assetID int64) (int64, error) @@ -367,6 +370,30 @@ func (r *assetRepository) IsExhibiting(assetID int64) (bool, error) { return count > 0, nil } +// GetExhibitingID 获取正在展出的展览ID(如果正在展出),返回0表示未展出 +func (r *assetRepository) GetExhibitingID(assetID int64) (int64, error) { + if assetID <= 0 { + return 0, errors.New("asset_id must be greater than 0") + } + + var exhibition struct { + ID int64 + } + err := r.db.Model(&models.Exhibition{}). + Select("id"). + Where("asset_id = ? AND expire_at > ?", assetID, time.Now().UnixMilli()). + First(&exhibition).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, nil // 未展出 + } + return 0, err + } + + return exhibition.ID, nil +} + // GetExhibitionStartTime 获取资产展出开始时间(如果正在展出) func (r *assetRepository) GetExhibitionStartTime(assetID int64) (int64, error) { if assetID <= 0 { diff --git a/backend/services/assetService/service/asset_like_service.go b/backend/services/assetService/service/asset_like_service.go index 4a4337f..06eca82 100644 --- a/backend/services/assetService/service/asset_like_service.go +++ b/backend/services/assetService/service/asset_like_service.go @@ -33,17 +33,20 @@ func NewAssetLikeService( } } -// isAssetExhibiting 检查资产当前是否在展出中 -func (s *AssetLikeService) isAssetExhibiting(assetID int64) (bool, error) { +// isAssetExhibiting 检查资产当前是否在展出中,返回 exhibition_id +func (s *AssetLikeService) isAssetExhibiting(assetID int64) (int64, error) { nowMs := time.Now().UnixMilli() - var count int64 - err := s.db.Table("exhibitions"). - Where("asset_id = ? AND expire_at > ?", assetID, nowMs). - Count(&count).Error - if err != nil { - return false, fmt.Errorf("failed to check exhibition status: %w", err) + var exhibition struct { + ID int64 } - return count > 0, nil + err := s.db.Table("exhibitions"). + Select("id"). + Where("asset_id = ? AND expire_at > ?", assetID, nowMs). + First(&exhibition).Error + if err != nil { + return 0, fmt.Errorf("failed to check exhibition status: %w", err) + } + return exhibition.ID, nil } // isUniqueConstraintViolation 检查错误是否为唯一约束冲突(PostgreSQL error code 23505) @@ -116,8 +119,8 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI return 0, fmt.Errorf("asset not found: %w", err) } - // 1.5 检查资产是否当前在展出中 - exhibiting, err := s.isAssetExhibiting(assetID) + // 1.5 检查资产是否当前在展出中,获取 exhibition_id + exhibitionID, err := s.isAssetExhibiting(assetID) if err != nil { logger.Logger.Error("Failed to check exhibition status", zap.Error(err), @@ -125,12 +128,12 @@ func (s *AssetLikeService) LikeAsset(ctx context.Context, assetID, userID, starI ) return 0, err } - if !exhibiting { + if exhibitionID == 0 { return 0, fmt.Errorf("资产未在展示中,无法点赞") } - // 2. 检查是否已点赞 - exists, err := s.assetLikeRepo.Exists(assetID, userID, starID) + // 2. 检查是否已点赞(同一展览同用户只能点赞一次) + exists, err := s.assetLikeRepo.Exists(assetID, userID, starID, exhibitionID) if err != nil { logger.Logger.Error("Failed to check if already liked", zap.Error(err), @@ -229,8 +232,8 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta zap.Int64("star_id", starID), ) - // 0. 检查资产是否当前在展出中(展出结束后不允许取消点赞) - exhibiting, err := s.isAssetExhibiting(assetID) + // 0. 检查资产是否当前在展出中,获取 exhibition_id(展出结束后不允许取消点赞) + exhibitionID, err := s.isAssetExhibiting(assetID) if err != nil { logger.Logger.Error("Failed to check exhibition status", zap.Error(err), @@ -238,12 +241,12 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta ) return 0, err } - if !exhibiting { + if exhibitionID == 0 { return 0, fmt.Errorf("资产未在展示中,无法取消点赞") } - // 1. 检查是否已点赞 - exists, err := s.assetLikeRepo.Exists(assetID, userID, starID) + // 1. 检查是否已点赞(同一展览同用户只能点赞一次) + exists, err := s.assetLikeRepo.Exists(assetID, userID, starID, exhibitionID) if err != nil { logger.Logger.Error("Failed to check if liked", zap.Error(err), @@ -262,8 +265,8 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta // 2. 开启事务 err = s.db.Transaction(func(tx *gorm.DB) error { - // 2.1 删除点赞记录 - if err := tx.Where("asset_id = ? AND user_id = ? AND star_id = ?", assetID, userID, starID). + // 2.1 删除点赞记录(按 exhibition_id 删除) + if err := tx.Where("asset_id = ? AND user_id = ? AND star_id = ? AND exhibition_id = ?", assetID, userID, starID, exhibitionID). Delete(&models.AssetLike{}).Error; err != nil { logger.Logger.Error("Failed to delete like record", zap.Error(err), @@ -336,7 +339,7 @@ func (s *AssetLikeService) UnlikeAsset(ctx context.Context, assetID, userID, sta return asset.LikeCount, nil } -// CheckAssetLike 检查是否已点赞 +// CheckAssetLike 检查是否已点赞(在当前展出中) func (s *AssetLikeService) CheckAssetLike(ctx context.Context, assetID, userID, starID int64) (bool, error) { logger.Logger.Debug("AssetLikeService.CheckAssetLike called", zap.Int64("asset_id", assetID), @@ -344,7 +347,22 @@ func (s *AssetLikeService) CheckAssetLike(ctx context.Context, assetID, userID, zap.Int64("star_id", starID), ) - exists, err := s.assetLikeRepo.Exists(assetID, userID, starID) + // 获取当前展出中的 exhibition_id + exhibitionID, err := s.isAssetExhibiting(assetID) + if err != nil { + logger.Logger.Error("Failed to check exhibition status", + zap.Error(err), + zap.Int64("asset_id", assetID), + ) + return false, fmt.Errorf("failed to check exhibition status: %w", err) + } + + if exhibitionID == 0 { + // 资产不在展出中,视为未点赞 + return false, nil + } + + exists, err := s.assetLikeRepo.Exists(assetID, userID, starID, exhibitionID) if err != nil { logger.Logger.Error("Failed to check like status", zap.Error(err), diff --git a/backend/services/assetService/service/asset_service.go b/backend/services/assetService/service/asset_service.go index 2d98a87..dc3519c 100644 --- a/backend/services/assetService/service/asset_service.go +++ b/backend/services/assetService/service/asset_service.go @@ -452,17 +452,21 @@ func (s *assetService) GetAsset(req *pb.GetAssetRequest, userID, starID int64) ( ownerNickname = profile.Nickname } - // 4. 检查当前用户是否已点赞 - isLiked, err := s.assetLikeRepo.Exists(asset.ID, userID, starID) - if err != nil { - logger.Logger.Warn("Failed to check like status, will return is_liked as false", - zap.Int64("asset_id", asset.ID), - zap.Int64("user_id", userID), - zap.Int64("star_id", starID), - zap.Error(err), - ) - // 检查失败时,默认为未点赞 - isLiked = false + // 4. 检查当前用户是否已点赞(需要获取当前展出中的 exhibition_id) + exhibitionID, _ := s.assetRepo.GetExhibitingID(asset.ID) + isLiked := false + if exhibitionID > 0 { + isLiked, err = s.assetLikeRepo.Exists(asset.ID, userID, starID, exhibitionID) + if err != nil { + logger.Logger.Warn("Failed to check like status, will return is_liked as false", + zap.Int64("asset_id", asset.ID), + zap.Int64("user_id", userID), + zap.Int64("star_id", starID), + zap.Error(err), + ) + // 检查失败时,默认为未点赞 + isLiked = false + } } // 5. 从 asset_registry 表获取 display_status diff --git a/backend/services/galleryService/service/cleanup_worker.go b/backend/services/galleryService/service/cleanup_worker.go index a6cf401..e90287e 100644 --- a/backend/services/galleryService/service/cleanup_worker.go +++ b/backend/services/galleryService/service/cleanup_worker.go @@ -156,17 +156,8 @@ func (w *CleanupWorker) cleanupExpiredExhibitions(now int64) { log.Printf("展品已到期并生成领取记录: ExhibitionID=%d, AssetID=%d, SlotID=%d, OccupierUID=%d, Revenue=%d", e.ID, e.AssetID, e.SlotID, e.OccupierUID, revenue) - // 5. 清除该资产的点赞记录(不阻断主流程),允许用户在下次展出时再次点赞 - assetID := e.AssetID - go func() { - if w.assetClient != nil { - if err := w.assetClient.ClearAssetLikeRecords(assetID); err != nil { - logger.Logger.Error("清除过期展品点赞记录失败", - zap.Int64("asset_id", assetID), - zap.Error(err)) - } - } - }() + // 5. 保留点赞记录,允许用户在下次展出时再次点赞(每次展览可点赞一次) + // 注意:点赞记录现在按 (asset_id, user_id, exhibition_id) 去重,不会与历史点赞冲突 } log.Printf("过期展品清理完成: 成功 %d 个, 失败 %d 个", successCount, failedCount) diff --git a/backend/services/socialService/repository/social_repository.go b/backend/services/socialService/repository/social_repository.go index c22b630..c9f2bc7 100644 --- a/backend/services/socialService/repository/social_repository.go +++ b/backend/services/socialService/repository/social_repository.go @@ -593,9 +593,10 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page // 计数查询(使用 DISTINCT 因为一个资产可能在多个展位展出) // 显示:展品未下架(deleted_at IS NULL),无论是否过期 // 不显示:已领取下架的(deleted_at IS NOT NULL)或从未展出过的 + // 使用 asset_likes.exhibition_id 关联,确保只返回有效展览的点赞记录 countQuery := r.db.Model(&models.AssetLike{}). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id AND e.deleted_at IS NULL"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id AND e.deleted_at IS NULL"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("a.deleted_at IS NULL AND a.is_active = ?", true) @@ -604,13 +605,14 @@ func (r *socialRepositoryImpl) GetMyLikedAssets(userID, starID int64, page, page } // 数据查询:只过滤已下架的,不过滤过期(用户领取收益后才下架) + // 使用 asset_likes.exhibition_id 关联,确保只返回当前有效展览的点赞记录 offset := (page - 1) * pageSize err := r.db.Model(&models.AssetLike{}). Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at as liked_at, (a.tags @> '["craft:lenticular"]') as is_lenticular`). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id AND e.deleted_at IS NULL"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id AND e.deleted_at IS NULL"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("a.deleted_at IS NULL AND a.is_active = ?", true). Group("asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at, is_lenticular"). @@ -692,7 +694,7 @@ func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page, countQuery := r.db.Model(&models.AssetLike{}). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.created_at >= ?", startOfDay). Where("a.deleted_at IS NULL AND a.is_active = ?", true). @@ -707,7 +709,7 @@ func (r *socialRepositoryImpl) GetMyTodayLikedAssets(userID, starID int64, page, Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at as liked_at`). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.created_at >= ?", startOfDay). Where("a.deleted_at IS NULL AND a.is_active = ?", true). @@ -752,7 +754,7 @@ func (r *socialRepositoryImpl) GetMyWeekLikedAssets(userID, starID int64, page, countQuery := r.db.Model(&models.AssetLike{}). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.created_at >= ?", startOfWeekMillis). Where("a.deleted_at IS NULL AND a.is_active = ?", true). @@ -767,7 +769,7 @@ func (r *socialRepositoryImpl) GetMyWeekLikedAssets(userID, starID int64, page, Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at as liked_at`). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("asset_likes.created_at >= ?", startOfWeekMillis). Where("a.deleted_at IS NULL AND a.is_active = ?", true). @@ -805,7 +807,7 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa countQuery := r.db.Model(&models.AssetLike{}). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("a.deleted_at IS NULL AND a.is_active = ?", true). Where("e.deleted_at IS NULL AND e.expire_at > ?", now) @@ -819,7 +821,7 @@ func (r *socialRepositoryImpl) GetUserLikedAssets(userID, starID int64, page, pa Select(`asset_likes.asset_id, a.name, a.cover_url, a.like_count, asset_likes.created_at as liked_at`). Joins("JOIN assets a ON a.id = asset_likes.asset_id"). - Joins("JOIN exhibitions e ON e.asset_id = a.id"). + Joins("JOIN exhibitions e ON e.id = asset_likes.exhibition_id"). Where("asset_likes.user_id = ? AND asset_likes.star_id = ?", userID, starID). Where("a.deleted_at IS NULL AND a.is_active = ?", true). Where("e.deleted_at IS NULL AND e.expire_at > ?", now).