diff --git a/internal/service/managercenter/service_test.go b/internal/service/managercenter/service_test.go index af32331..ecb9333 100644 --- a/internal/service/managercenter/service_test.go +++ b/internal/service/managercenter/service_test.go @@ -375,6 +375,48 @@ func TestSendVIPUsesVIPLevelConfig(t *testing.T) { } } +func TestVIPGiftCleanupExpiresOldVIPResourcesOnly(t *testing.T) { + service, _ := newManagerCenterTestService(t) + db := service.db.(*gorm.DB) + if err := db.AutoMigrate(&managerVIPCleanupPropsRow{}, &managerVIPCleanupBadgeRow{}); err != nil { + t.Fatalf("auto migrate cleanup tables: %v", err) + } + now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC) + future := now.AddDate(0, 0, 20) + userID := int64(2) + + seedManagerVIPCleanupProps(t, db, + managerVIPCleanupPropsRow{ID: 1, UserID: userID, PropsID: 101, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, + managerVIPCleanupPropsRow{ID: 2, UserID: userID, PropsID: 201, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, + managerVIPCleanupPropsRow{ID: 3, UserID: userID, PropsID: 999, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, + managerVIPCleanupPropsRow{ID: 4, UserID: userID, PropsID: 103, Type: propsTypeDataCard, ExpireTime: future, UseProps: true, AllowGive: true}, + ) + seedManagerVIPCleanupBadges(t, db, + managerVIPCleanupBadgeRow{ID: 1, UserID: userID, BadgeID: 100, ExpireTime: future, UseProps: true}, + managerVIPCleanupBadgeRow{ID: 2, UserID: userID, BadgeID: 200, ExpireTime: future, UseProps: true}, + ) + + target := vipGiftResourcesFromConfig(model.VipLevelConfig{ + BadgeResourceID: 200, + AvatarFrameResourceID: 201, + }) + all := append(vipGiftResourcesFromConfig(model.VipLevelConfig{ + BadgeResourceID: 100, + AvatarFrameResourceID: 101, + BackgroundCardResourceID: 103, + }), target...) + if err := service.cleanupSupersededVIPGiftResources(context.Background(), userID, target, all, now); err != nil { + t.Fatalf("cleanupSupersededVIPGiftResources() error = %v", err) + } + + assertManagerVIPCleanupProps(t, db, 101, now, false, false) + assertManagerVIPCleanupProps(t, db, 103, now, false, false) + assertManagerVIPCleanupProps(t, db, 201, future, true, true) + assertManagerVIPCleanupProps(t, db, 999, future, true, true) + assertManagerVIPCleanupBadge(t, db, 100, now, false) + assertManagerVIPCleanupBadge(t, db, 200, future, true) +} + func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) @@ -400,6 +442,77 @@ func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) { assertAppErrorCode(t, err, "vip_not_giftable") } +type managerVIPCleanupPropsRow struct { + ID int64 `gorm:"column:id;primaryKey"` + UserID int64 `gorm:"column:user_id"` + PropsID int64 `gorm:"column:props_id"` + Type string `gorm:"column:type"` + ExpireTime time.Time `gorm:"column:expire_time"` + UseProps bool `gorm:"column:is_use_props"` + AllowGive bool `gorm:"column:allow_give"` + UpdateTime time.Time `gorm:"column:update_time"` +} + +func (managerVIPCleanupPropsRow) TableName() string { + return "user_props_backpack" +} + +type managerVIPCleanupBadgeRow struct { + ID int64 `gorm:"column:id;primaryKey"` + UserID int64 `gorm:"column:user_id"` + BadgeID int64 `gorm:"column:badge_id"` + ExpireType string `gorm:"column:expire_type"` + ExpireTime time.Time `gorm:"column:expire_time"` + UseProps bool `gorm:"column:is_use_props"` + UpdateTime time.Time `gorm:"column:update_time"` +} + +func (managerVIPCleanupBadgeRow) TableName() string { + return "user_badge_backpack" +} + +func seedManagerVIPCleanupProps(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupPropsRow) { + t.Helper() + for _, row := range rows { + if err := db.Create(&row).Error; err != nil { + t.Fatalf("seed manager cleanup props: %v", err) + } + } +} + +func seedManagerVIPCleanupBadges(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupBadgeRow) { + t.Helper() + for _, row := range rows { + if err := db.Create(&row).Error; err != nil { + t.Fatalf("seed manager cleanup badge: %v", err) + } + } +} + +func assertManagerVIPCleanupProps(t *testing.T, db *gorm.DB, propsID int64, expireTime time.Time, useProps bool, allowGive bool) { + t.Helper() + var row managerVIPCleanupPropsRow + if err := db.Where("props_id = ?", propsID).First(&row).Error; err != nil { + t.Fatalf("load manager cleanup props %d: %v", propsID, err) + } + if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps || row.AllowGive != allowGive { + t.Fatalf("props %d = expire:%s use:%v allow:%v, want expire:%s use:%v allow:%v", + propsID, row.ExpireTime, row.UseProps, row.AllowGive, expireTime, useProps, allowGive) + } +} + +func assertManagerVIPCleanupBadge(t *testing.T, db *gorm.DB, badgeID int64, expireTime time.Time, useProps bool) { + t.Helper() + var row managerVIPCleanupBadgeRow + if err := db.Where("badge_id = ?", badgeID).First(&row).Error; err != nil { + t.Fatalf("load manager cleanup badge %d: %v", badgeID, err) + } + if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps { + t.Fatalf("badge %d = expire:%s use:%v, want expire:%s use:%v", + badgeID, row.ExpireTime, row.UseProps, expireTime, useProps) + } +} + func TestBanUserRejectsRechargedUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) diff --git a/internal/service/managercenter/vip_gift.go b/internal/service/managercenter/vip_gift.go index a9da3fd..f92dbe1 100644 --- a/internal/service/managercenter/vip_gift.go +++ b/internal/service/managercenter/vip_gift.go @@ -171,6 +171,7 @@ func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target user CreateTime: now, } } + previousResources := vipGiftResourcesFromState(state) applyVIPGiftConfigToState(state, config, startAt, expireAt, now) if err := tx.Save(state).Error; err != nil { return nil, serverError("vip_state_save_failed", err.Error()) @@ -209,7 +210,7 @@ func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target user } committed = true - if err := s.grantVIPGiftResources(ctx, target.ID, manager.UserID, config, expireAt, now); err != nil { + if err := s.grantVIPGiftResources(ctx, sysOrigin, target.ID, manager.UserID, config, expireAt, previousResources, now); err != nil { return nil, serverError("vip_resource_grant_failed", err.Error()) } @@ -301,11 +302,8 @@ func applyVIPGiftConfigToState(state *model.UserVipState, config model.VipLevelC } } -func (s *Service) grantVIPGiftResources(ctx context.Context, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, now time.Time) error { +func (s *Service) grantVIPGiftResources(ctx context.Context, sysOrigin string, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, additionalOldResources []vipGiftResource, now time.Time) error { resources := vipGiftResourcesFromConfig(config) - if len(resources) == 0 { - return nil - } days := config.DurationDays if days <= 0 { days = defaultVIPGiftDays @@ -337,6 +335,14 @@ func (s *Service) grantVIPGiftResources(ctx context.Context, userID int64, manag return err } } + allResources, err := s.vipGiftResourcesFromSysOrigin(ctx, sysOrigin) + if err != nil { + return err + } + allResources = append(allResources, additionalOldResources...) + if err := s.cleanupSupersededVIPGiftResources(ctx, userID, resources, allResources, now); err != nil { + return err + } if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil { return fmt.Errorf("remove vip user profile cache: %w", err) } @@ -374,6 +380,7 @@ func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource { {resourceID: config.EntryEffectResourceID, propsType: propsTypeRide}, {resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble}, {resourceID: config.FloatPictureResourceID, propsType: propsTypeFloatPicture}, + {resourceID: config.BackgroundCardResourceID, propsType: propsTypeDataCard}, } out := make([]vipGiftResource, 0, len(resources)) for _, resource := range resources { @@ -384,6 +391,104 @@ func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource { return out } +func vipGiftResourcesFromState(state *model.UserVipState) []vipGiftResource { + if state == nil { + return nil + } + return compactVIPGiftResources([]vipGiftResource{ + {resourceID: state.BadgeResourceID, propsType: propsTypeBadge, badge: true}, + {resourceID: state.ShortBadgeResourceID, propsType: propsTypeBadge, badge: true}, + {resourceID: state.AvatarFrameResourceID, propsType: propsTypeAvatarFrame}, + {resourceID: state.EntryEffectResourceID, propsType: propsTypeRide}, + {resourceID: state.ChatBubbleResourceID, propsType: propsTypeChatBubble}, + {resourceID: state.FloatPictureResourceID, propsType: propsTypeFloatPicture}, + {resourceID: state.BackgroundCardResourceID, propsType: propsTypeDataCard}, + }) +} + +func compactVIPGiftResources(resources []vipGiftResource) []vipGiftResource { + out := make([]vipGiftResource, 0, len(resources)) + for _, resource := range resources { + if resource.resourceID > 0 { + out = append(out, resource) + } + } + return out +} + +func (s *Service) vipGiftResourcesFromSysOrigin(ctx context.Context, sysOrigin string) ([]vipGiftResource, error) { + var configs []model.VipLevelConfig + if err := s.db.WithContext(ctx). + Where("sys_origin = ?", normalizeSysOrigin(sysOrigin)). + Find(&configs).Error; err != nil { + return nil, err + } + resources := make([]vipGiftResource, 0, len(configs)*7) + for _, config := range configs { + resources = append(resources, vipGiftResourcesFromConfig(config)...) + } + return resources, nil +} + +func (s *Service) cleanupSupersededVIPGiftResources(ctx context.Context, userID int64, targetResources []vipGiftResource, allVIPResources []vipGiftResource, now time.Time) error { + targets := make(map[string]struct{}, len(targetResources)) + for _, resource := range targetResources { + targets[vipGiftResourceKey(resource)] = struct{}{} + } + + badgeIDs := make([]int64, 0) + propsIDsByType := map[string][]int64{} + seen := make(map[string]struct{}, len(allVIPResources)) + for _, resource := range allVIPResources { + if resource.resourceID <= 0 { + continue + } + key := vipGiftResourceKey(resource) + if _, exists := targets[key]; exists { + continue + } + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + if resource.badge { + badgeIDs = append(badgeIDs, resource.resourceID) + continue + } + if strings.TrimSpace(resource.propsType) != "" { + propsIDsByType[resource.propsType] = append(propsIDsByType[resource.propsType], resource.resourceID) + } + } + + if len(badgeIDs) > 0 { + if err := s.db.WithContext(ctx).Exec( + "UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id IN ? AND expire_time > ?", + "TEMPORARY", now, false, now, userID, badgeIDs, now, + ).Error; err != nil { + return err + } + } + for propsType, propsIDs := range propsIDsByType { + if len(propsIDs) == 0 { + continue + } + if err := s.db.WithContext(ctx).Exec( + "UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id IN ? AND type = ? AND expire_time > ?", + now, false, false, now, userID, propsIDs, propsType, now, + ).Error; err != nil { + return err + } + } + return nil +} + +func vipGiftResourceKey(resource vipGiftResource) string { + if resource.badge { + return fmt.Sprintf("BADGE:%d", resource.resourceID) + } + return fmt.Sprintf("%s:%d", resource.propsType, resource.resourceID) +} + func (s *Service) managerApplicantName(ctx context.Context, managerID int64) string { row, err := s.loadUserRowByID(ctx, managerID) if err != nil { diff --git a/internal/service/vip/app.go b/internal/service/vip/app.go index 9066ce3..5af0780 100644 --- a/internal/service/vip/app.go +++ b/internal/service/vip/app.go @@ -170,7 +170,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque if stateErr != nil { return nil, stateErr } - if err := s.ensureStateResourcesGranted(ctx, user.UserID, state, now); err != nil { + if err := s.ensureStateResourcesGranted(ctx, sysOrigin, user.UserID, state, now); err != nil { return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error()) } resources, resourceErr := s.loadResourceLookup(ctx, appendResourceIDsFromState(nil, state)) @@ -274,6 +274,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque } } + previousResources := vipGrantResourcesFromState(state) applyPlanToState(state, plan, now) order.Status = orderStatusSuccess order.ErrorMessage = "" @@ -289,7 +290,7 @@ func (s *Service) Purchase(ctx context.Context, user AuthUser, req PurchaseReque } committed = true - if err := s.ensurePlanResourcesGranted(ctx, user.UserID, plan, now); err != nil { + if err := s.ensurePlanResourcesGranted(ctx, sysOrigin, user.UserID, plan, configs, previousResources, now); err != nil { return nil, NewAppError(http.StatusBadGateway, "vip_resource_grant_failed", err.Error()) } @@ -536,15 +537,28 @@ func expireState(state *model.UserVipState, now time.Time) { state.UpdateTime = now } -func (s *Service) ensurePlanResourcesGranted(ctx context.Context, userID int64, plan purchasePlan, now time.Time) error { - return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromConfig(plan.TargetConfig), plan.ExpireAt, plan.DurationDays, now) +func (s *Service) ensurePlanResourcesGranted(ctx context.Context, sysOrigin string, userID int64, plan purchasePlan, configs map[int]model.VipLevelConfig, additionalOldResources []vipGrantResource, now time.Time) error { + targetResources := vipGrantResourcesFromConfig(plan.TargetConfig) + if err := s.ensureVIPResourcesGranted(ctx, userID, targetResources, plan.ExpireAt, plan.DurationDays, now); err != nil { + return err + } + allVIPResources := append(vipGrantResourcesFromConfigs(configs), additionalOldResources...) + return s.cleanupSupersededVIPResources(ctx, userID, targetResources, allVIPResources, now) } -func (s *Service) ensureStateResourcesGranted(ctx context.Context, userID int64, state *model.UserVipState, now time.Time) error { +func (s *Service) ensureStateResourcesGranted(ctx context.Context, sysOrigin string, userID int64, state *model.UserVipState, now time.Time) error { if !isActiveState(state, now) { return nil } - return s.ensureVIPResourcesGranted(ctx, userID, vipGrantResourcesFromState(state), state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now) + targetResources := vipGrantResourcesFromState(state) + if err := s.ensureVIPResourcesGranted(ctx, userID, targetResources, state.ExpireAt, vipGrantDaysUntil(now, state.ExpireAt), now); err != nil { + return err + } + configs, err := s.loadConfigMap(ctx, sysOrigin) + if err != nil { + return err + } + return s.cleanupSupersededVIPResources(ctx, userID, targetResources, vipGrantResourcesFromConfigs(configs), now) } func (s *Service) ensureVIPResourcesGranted(ctx context.Context, userID int64, resources []vipGrantResource, expireAt time.Time, days int, now time.Time) error { @@ -685,6 +699,17 @@ func vipGrantResourcesFromState(state *model.UserVipState) []vipGrantResource { }) } +func vipGrantResourcesFromConfigs(configs map[int]model.VipLevelConfig) []vipGrantResource { + if len(configs) == 0 { + return nil + } + resources := make([]vipGrantResource, 0, len(configs)*7) + for _, config := range configs { + resources = append(resources, vipGrantResourcesFromConfig(config)...) + } + return compactVIPGrantResources(resources) +} + func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource { compacted := make([]vipGrantResource, 0, len(resources)) for _, resource := range resources { @@ -695,6 +720,65 @@ func compactVIPGrantResources(resources []vipGrantResource) []vipGrantResource { return compacted } +func (s *Service) cleanupSupersededVIPResources(ctx context.Context, userID int64, targetResources []vipGrantResource, allVIPResources []vipGrantResource, now time.Time) error { + targets := make(map[string]struct{}, len(targetResources)) + for _, resource := range targetResources { + targets[vipGrantResourceKey(resource)] = struct{}{} + } + + badgeIDs := make([]int64, 0) + propsIDsByType := map[string][]int64{} + seen := make(map[string]struct{}, len(allVIPResources)) + for _, resource := range allVIPResources { + if resource.ResourceID <= 0 { + continue + } + key := vipGrantResourceKey(resource) + if _, exists := targets[key]; exists { + continue + } + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + if resource.Badge { + badgeIDs = append(badgeIDs, resource.ResourceID) + continue + } + if strings.TrimSpace(resource.PropsType) != "" { + propsIDsByType[resource.PropsType] = append(propsIDsByType[resource.PropsType], resource.ResourceID) + } + } + + if len(badgeIDs) > 0 { + if err := s.db.WithContext(ctx).Exec( + "UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id IN ? AND expire_time > ?", + "TEMPORARY", now, false, now, userID, badgeIDs, now, + ).Error; err != nil { + return err + } + } + for propsType, propsIDs := range propsIDsByType { + if len(propsIDs) == 0 { + continue + } + if err := s.db.WithContext(ctx).Exec( + "UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id IN ? AND type = ? AND expire_time > ?", + now, false, false, now, userID, propsIDs, propsType, now, + ).Error; err != nil { + return err + } + } + return nil +} + +func vipGrantResourceKey(resource vipGrantResource) string { + if resource.Badge { + return fmt.Sprintf("BADGE:%d", resource.ResourceID) + } + return fmt.Sprintf("%s:%d", resource.PropsType, resource.ResourceID) +} + func vipGrantDaysUntil(now time.Time, expireAt time.Time) int { if !expireAt.After(now) { return 1 diff --git a/internal/service/vip/purchase_test.go b/internal/service/vip/purchase_test.go index 8bfc5ff..aa7144a 100644 --- a/internal/service/vip/purchase_test.go +++ b/internal/service/vip/purchase_test.go @@ -1,6 +1,7 @@ package vip import ( + "context" "encoding/json" "errors" "net/http" @@ -9,6 +10,9 @@ import ( "time" "chatapp3-golang/internal/model" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestBuildPurchasePlanNewPurchase(t *testing.T) { @@ -322,6 +326,47 @@ func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) { assertGrant(6, 16, vipPropsTypeDataCard, false) } +func TestCleanupSupersededVIPResourcesExpiresOnlyOldVIPResources(t *testing.T) { + db := openVIPCleanupTestDB(t) + service := &Service{db: db} + now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC) + future := now.AddDate(0, 0, 20) + oldTime := now.AddDate(0, 0, -1) + userID := int64(1001) + + seedVIPCleanupRows(t, db, + vipCleanupPropsRow{ID: 1, UserID: userID, PropsID: 101, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime}, + vipCleanupPropsRow{ID: 2, UserID: userID, PropsID: 201, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime}, + vipCleanupPropsRow{ID: 3, UserID: userID, PropsID: 999, Type: vipPropsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime}, + vipCleanupPropsRow{ID: 4, UserID: userID, PropsID: 102, Type: vipPropsTypeRide, ExpireTime: future, UseProps: true, AllowGive: true, UpdateTime: oldTime}, + ) + seedVIPCleanupBadges(t, db, + vipCleanupBadgeRow{ID: 1, UserID: userID, BadgeID: 100, ExpireTime: future, UseProps: true, UpdateTime: oldTime}, + vipCleanupBadgeRow{ID: 2, UserID: userID, BadgeID: 200, ExpireTime: future, UseProps: true, UpdateTime: oldTime}, + ) + + target := vipGrantResourcesFromConfig(model.VipLevelConfig{ + BadgeResourceID: 200, + AvatarFrameResourceID: 201, + }) + all := append(vipGrantResourcesFromConfig(model.VipLevelConfig{ + BadgeResourceID: 100, + AvatarFrameResourceID: 101, + EntryEffectResourceID: 102, + }), target...) + + if err := service.cleanupSupersededVIPResources(context.Background(), userID, target, all, now); err != nil { + t.Fatalf("cleanupSupersededVIPResources() error = %v", err) + } + + assertVIPCleanupProps(t, db, 101, now, false, false) + assertVIPCleanupProps(t, db, 102, now, false, false) + assertVIPCleanupProps(t, db, 201, future, true, true) + assertVIPCleanupProps(t, db, 999, future, true, true) + assertVIPCleanupBadge(t, db, 100, now, false) + assertVIPCleanupBadge(t, db, 200, future, true) +} + func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) { now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC) expireAt := now.Add((24 * time.Hour) + time.Second) @@ -334,6 +379,89 @@ func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) { } } +type vipCleanupPropsRow struct { + ID int64 `gorm:"column:id;primaryKey"` + UserID int64 `gorm:"column:user_id"` + PropsID int64 `gorm:"column:props_id"` + Type string `gorm:"column:type"` + ExpireTime time.Time `gorm:"column:expire_time"` + UseProps bool `gorm:"column:is_use_props"` + AllowGive bool `gorm:"column:allow_give"` + UpdateTime time.Time `gorm:"column:update_time"` +} + +func (vipCleanupPropsRow) TableName() string { + return "user_props_backpack" +} + +type vipCleanupBadgeRow struct { + ID int64 `gorm:"column:id;primaryKey"` + UserID int64 `gorm:"column:user_id"` + BadgeID int64 `gorm:"column:badge_id"` + ExpireType string `gorm:"column:expire_type"` + ExpireTime time.Time `gorm:"column:expire_time"` + UseProps bool `gorm:"column:is_use_props"` + UpdateTime time.Time `gorm:"column:update_time"` +} + +func (vipCleanupBadgeRow) TableName() string { + return "user_badge_backpack" +} + +func openVIPCleanupTestDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + if err := db.AutoMigrate(&vipCleanupPropsRow{}, &vipCleanupBadgeRow{}); err != nil { + t.Fatalf("auto migrate cleanup tables: %v", err) + } + return db +} + +func seedVIPCleanupRows(t *testing.T, db *gorm.DB, rows ...vipCleanupPropsRow) { + t.Helper() + for _, row := range rows { + if err := db.Create(&row).Error; err != nil { + t.Fatalf("seed props row: %v", err) + } + } +} + +func seedVIPCleanupBadges(t *testing.T, db *gorm.DB, rows ...vipCleanupBadgeRow) { + t.Helper() + for _, row := range rows { + if err := db.Create(&row).Error; err != nil { + t.Fatalf("seed badge row: %v", err) + } + } +} + +func assertVIPCleanupProps(t *testing.T, db *gorm.DB, propsID int64, expireTime time.Time, useProps bool, allowGive bool) { + t.Helper() + var row vipCleanupPropsRow + if err := db.Where("props_id = ?", propsID).First(&row).Error; err != nil { + t.Fatalf("load props %d: %v", propsID, err) + } + if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps || row.AllowGive != allowGive { + t.Fatalf("props %d = expire:%s use:%v allow:%v, want expire:%s use:%v allow:%v", + propsID, row.ExpireTime, row.UseProps, row.AllowGive, expireTime, useProps, allowGive) + } +} + +func assertVIPCleanupBadge(t *testing.T, db *gorm.DB, badgeID int64, expireTime time.Time, useProps bool) { + t.Helper() + var row vipCleanupBadgeRow + if err := db.Where("badge_id = ?", badgeID).First(&row).Error; err != nil { + t.Fatalf("load badge %d: %v", badgeID, err) + } + if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps { + t.Fatalf("badge %d = expire:%s use:%v, want expire:%s use:%v", + badgeID, row.ExpireTime, row.UseProps, expireTime, useProps) + } +} + func vipTestConfigs() map[int]model.VipLevelConfig { configs := make(map[int]model.VipLevelConfig, levelCount) for level := 1; level <= levelCount; level++ {