From 0d8e6f599bac0858274d72455d302245b2c97a9b Mon Sep 17 00:00:00 2001 From: hy001 Date: Sat, 16 May 2026 02:21:43 +0800 Subject: [PATCH] feat: add VIP badge card resources --- internal/model/vip_models.go | 120 +++++---- internal/service/vip/admin.go | 51 ++-- internal/service/vip/app.go | 22 ++ internal/service/vip/config.go | 27 +- internal/service/vip/purchase_test.go | 88 ++++-- internal/service/vip/resources.go | 10 +- internal/service/vip/types.go | 99 ++++--- .../042_vip_badge_card_effect_resources.sql | 253 ++++++++++++++++++ 8 files changed, 524 insertions(+), 146 deletions(-) create mode 100644 migrations/042_vip_badge_card_effect_resources.sql diff --git a/internal/model/vip_models.go b/internal/model/vip_models.go index 92a21d5..2ac29b1 100644 --- a/internal/model/vip_models.go +++ b/internal/model/vip_models.go @@ -4,31 +4,40 @@ import "time" // VipLevelConfig stores the operator-managed 30-day VIP level configuration. type VipLevelConfig struct { - ID int64 `gorm:"column:id;primaryKey"` - SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_vip_level_config,priority:1;index:idx_vip_level_config_sys_origin,priority:1"` - Level int `gorm:"column:level;uniqueIndex:uk_vip_level_config,priority:2"` - LevelCode string `gorm:"column:level_code;size:16"` - DisplayName string `gorm:"column:display_name;size:64"` - Enabled bool `gorm:"column:enabled"` - DurationDays int `gorm:"column:duration_days"` - PriceGold int64 `gorm:"column:price_gold"` - BadgeResourceID int64 `gorm:"column:badge_resource_id"` - BadgeName string `gorm:"column:badge_name;size:128"` - BadgeURL string `gorm:"column:badge_url;size:512"` - AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"` - AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"` - AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"` - EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"` - EntryEffectName string `gorm:"column:entry_effect_name;size:128"` - EntryEffectURL string `gorm:"column:entry_effect_url;size:512"` - ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"` - ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"` - ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"` - FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"` - FloatPictureName string `gorm:"column:float_picture_name;size:128"` - FloatPictureURL string `gorm:"column:float_picture_url;size:512"` - CreateTime time.Time `gorm:"column:create_time"` - UpdateTime time.Time `gorm:"column:update_time"` + ID int64 `gorm:"column:id;primaryKey"` + SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_vip_level_config,priority:1;index:idx_vip_level_config_sys_origin,priority:1"` + Level int `gorm:"column:level;uniqueIndex:uk_vip_level_config,priority:2"` + LevelCode string `gorm:"column:level_code;size:16"` + DisplayName string `gorm:"column:display_name;size:64"` + Enabled bool `gorm:"column:enabled"` + DurationDays int `gorm:"column:duration_days"` + PriceGold int64 `gorm:"column:price_gold"` + BadgeResourceID int64 `gorm:"column:badge_resource_id"` + BadgeName string `gorm:"column:badge_name;size:128"` + BadgeURL string `gorm:"column:badge_url;size:512"` + ShortBadgeResourceID int64 `gorm:"column:short_badge_resource_id"` + ShortBadgeName string `gorm:"column:short_badge_name;size:128"` + ShortBadgeURL string `gorm:"column:short_badge_url;size:512"` + AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"` + AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"` + AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"` + EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"` + EntryEffectName string `gorm:"column:entry_effect_name;size:128"` + EntryEffectURL string `gorm:"column:entry_effect_url;size:512"` + ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"` + ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"` + ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"` + FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"` + FloatPictureName string `gorm:"column:float_picture_name;size:128"` + FloatPictureURL string `gorm:"column:float_picture_url;size:512"` + BackgroundCardResourceID int64 `gorm:"column:background_card_resource_id"` + BackgroundCardName string `gorm:"column:background_card_name;size:128"` + BackgroundCardURL string `gorm:"column:background_card_url;size:512"` + EffectImageResourceID int64 `gorm:"column:effect_image_resource_id"` + EffectImageName string `gorm:"column:effect_image_name;size:128"` + EffectImageURL string `gorm:"column:effect_image_url;size:512"` + CreateTime time.Time `gorm:"column:create_time"` + UpdateTime time.Time `gorm:"column:update_time"` } // TableName returns the VIP level config table name. @@ -36,32 +45,41 @@ func (VipLevelConfig) TableName() string { return "vip_level_config" } // UserVipState stores the single active VIP state for a user. type UserVipState struct { - ID int64 `gorm:"column:id;primaryKey"` - SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_user_vip_state,priority:1;index:idx_user_vip_state_lookup,priority:1"` - UserID int64 `gorm:"column:user_id;uniqueIndex:uk_user_vip_state,priority:2;index:idx_user_vip_state_lookup,priority:2"` - Level int `gorm:"column:level"` - LevelCode string `gorm:"column:level_code;size:16"` - DisplayName string `gorm:"column:display_name;size:64"` - Status string `gorm:"column:status;size:32;index:idx_user_vip_state_lookup,priority:3"` - StartAt time.Time `gorm:"column:start_at"` - ExpireAt time.Time `gorm:"column:expire_at;index:idx_user_vip_expire"` - BadgeResourceID int64 `gorm:"column:badge_resource_id"` - BadgeName string `gorm:"column:badge_name;size:128"` - BadgeURL string `gorm:"column:badge_url;size:512"` - AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"` - AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"` - AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"` - EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"` - EntryEffectName string `gorm:"column:entry_effect_name;size:128"` - EntryEffectURL string `gorm:"column:entry_effect_url;size:512"` - ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"` - ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"` - ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"` - FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"` - FloatPictureName string `gorm:"column:float_picture_name;size:128"` - FloatPictureURL string `gorm:"column:float_picture_url;size:512"` - CreateTime time.Time `gorm:"column:create_time"` - UpdateTime time.Time `gorm:"column:update_time"` + ID int64 `gorm:"column:id;primaryKey"` + SysOrigin string `gorm:"column:sys_origin;size:32;uniqueIndex:uk_user_vip_state,priority:1;index:idx_user_vip_state_lookup,priority:1"` + UserID int64 `gorm:"column:user_id;uniqueIndex:uk_user_vip_state,priority:2;index:idx_user_vip_state_lookup,priority:2"` + Level int `gorm:"column:level"` + LevelCode string `gorm:"column:level_code;size:16"` + DisplayName string `gorm:"column:display_name;size:64"` + Status string `gorm:"column:status;size:32;index:idx_user_vip_state_lookup,priority:3"` + StartAt time.Time `gorm:"column:start_at"` + ExpireAt time.Time `gorm:"column:expire_at;index:idx_user_vip_expire"` + BadgeResourceID int64 `gorm:"column:badge_resource_id"` + BadgeName string `gorm:"column:badge_name;size:128"` + BadgeURL string `gorm:"column:badge_url;size:512"` + ShortBadgeResourceID int64 `gorm:"column:short_badge_resource_id"` + ShortBadgeName string `gorm:"column:short_badge_name;size:128"` + ShortBadgeURL string `gorm:"column:short_badge_url;size:512"` + AvatarFrameResourceID int64 `gorm:"column:avatar_frame_resource_id"` + AvatarFrameName string `gorm:"column:avatar_frame_name;size:128"` + AvatarFrameURL string `gorm:"column:avatar_frame_url;size:512"` + EntryEffectResourceID int64 `gorm:"column:entry_effect_resource_id"` + EntryEffectName string `gorm:"column:entry_effect_name;size:128"` + EntryEffectURL string `gorm:"column:entry_effect_url;size:512"` + ChatBubbleResourceID int64 `gorm:"column:chat_bubble_resource_id"` + ChatBubbleName string `gorm:"column:chat_bubble_name;size:128"` + ChatBubbleURL string `gorm:"column:chat_bubble_url;size:512"` + FloatPictureResourceID int64 `gorm:"column:float_picture_resource_id"` + FloatPictureName string `gorm:"column:float_picture_name;size:128"` + FloatPictureURL string `gorm:"column:float_picture_url;size:512"` + BackgroundCardResourceID int64 `gorm:"column:background_card_resource_id"` + BackgroundCardName string `gorm:"column:background_card_name;size:128"` + BackgroundCardURL string `gorm:"column:background_card_url;size:512"` + EffectImageResourceID int64 `gorm:"column:effect_image_resource_id"` + EffectImageName string `gorm:"column:effect_image_name;size:128"` + EffectImageURL string `gorm:"column:effect_image_url;size:512"` + CreateTime time.Time `gorm:"column:create_time"` + UpdateTime time.Time `gorm:"column:update_time"` } // TableName returns the user VIP state table name. diff --git a/internal/service/vip/admin.go b/internal/service/vip/admin.go index 13f04ab..45c78e6 100644 --- a/internal/service/vip/admin.go +++ b/internal/service/vip/admin.go @@ -105,7 +105,7 @@ func (s *Service) PageUserStates( Find(&rows).Error; err != nil { return nil, err } - resourceIDs := make([]int64, 0, len(rows)*5) + resourceIDs := make([]int64, 0, len(rows)*8) for idx := range rows { resourceIDs = appendResourceIDsFromState(resourceIDs, &rows[idx]) } @@ -133,26 +133,35 @@ func (s *Service) PageUserOrders(ctx context.Context, user AuthUser, cursor int, func (s *Service) expireDueStates(ctx context.Context, sysOrigin string, now time.Time) error { updates := map[string]any{ - "level": 0, - "level_code": "", - "display_name": "", - "status": statusExpired, - "badge_resource_id": 0, - "badge_name": "", - "badge_url": "", - "avatar_frame_resource_id": 0, - "avatar_frame_name": "", - "avatar_frame_url": "", - "entry_effect_resource_id": 0, - "entry_effect_name": "", - "entry_effect_url": "", - "chat_bubble_resource_id": 0, - "chat_bubble_name": "", - "chat_bubble_url": "", - "float_picture_resource_id": 0, - "float_picture_name": "", - "float_picture_url": "", - "update_time": now, + "level": 0, + "level_code": "", + "display_name": "", + "status": statusExpired, + "badge_resource_id": 0, + "badge_name": "", + "badge_url": "", + "short_badge_resource_id": 0, + "short_badge_name": "", + "short_badge_url": "", + "avatar_frame_resource_id": 0, + "avatar_frame_name": "", + "avatar_frame_url": "", + "entry_effect_resource_id": 0, + "entry_effect_name": "", + "entry_effect_url": "", + "chat_bubble_resource_id": 0, + "chat_bubble_name": "", + "chat_bubble_url": "", + "float_picture_resource_id": 0, + "float_picture_name": "", + "float_picture_url": "", + "background_card_resource_id": 0, + "background_card_name": "", + "background_card_url": "", + "effect_image_resource_id": 0, + "effect_image_name": "", + "effect_image_url": "", + "update_time": now, } return s.db.WithContext(ctx). Model(&model.UserVipState{}). diff --git a/internal/service/vip/app.go b/internal/service/vip/app.go index 2e17331..9066ce3 100644 --- a/internal/service/vip/app.go +++ b/internal/service/vip/app.go @@ -477,6 +477,9 @@ func applyPlanToState(state *model.UserVipState, plan purchasePlan, now time.Tim state.BadgeResourceID = plan.TargetConfig.BadgeResourceID state.BadgeName = "" state.BadgeURL = "" + state.ShortBadgeResourceID = plan.TargetConfig.ShortBadgeResourceID + state.ShortBadgeName = "" + state.ShortBadgeURL = "" state.AvatarFrameResourceID = plan.TargetConfig.AvatarFrameResourceID state.AvatarFrameName = "" state.AvatarFrameURL = "" @@ -489,6 +492,12 @@ func applyPlanToState(state *model.UserVipState, plan purchasePlan, now time.Tim state.FloatPictureResourceID = plan.TargetConfig.FloatPictureResourceID state.FloatPictureName = "" state.FloatPictureURL = "" + state.BackgroundCardResourceID = plan.TargetConfig.BackgroundCardResourceID + state.BackgroundCardName = "" + state.BackgroundCardURL = "" + state.EffectImageResourceID = plan.TargetConfig.EffectImageResourceID + state.EffectImageName = "" + state.EffectImageURL = "" state.UpdateTime = now if state.CreateTime.IsZero() { state.CreateTime = now @@ -503,6 +512,9 @@ func expireState(state *model.UserVipState, now time.Time) { state.BadgeResourceID = 0 state.BadgeName = "" state.BadgeURL = "" + state.ShortBadgeResourceID = 0 + state.ShortBadgeName = "" + state.ShortBadgeURL = "" state.AvatarFrameResourceID = 0 state.AvatarFrameName = "" state.AvatarFrameURL = "" @@ -515,6 +527,12 @@ func expireState(state *model.UserVipState, now time.Time) { state.FloatPictureResourceID = 0 state.FloatPictureName = "" state.FloatPictureURL = "" + state.BackgroundCardResourceID = 0 + state.BackgroundCardName = "" + state.BackgroundCardURL = "" + state.EffectImageResourceID = 0 + state.EffectImageName = "" + state.EffectImageURL = "" state.UpdateTime = now } @@ -643,10 +661,12 @@ func (s *Service) alignVIPResourceExpireAt(ctx context.Context, userID int64, re func vipGrantResourcesFromConfig(config model.VipLevelConfig) []vipGrantResource { return compactVIPGrantResources([]vipGrantResource{ {ResourceID: config.BadgeResourceID, Badge: true}, + {ResourceID: config.ShortBadgeResourceID, Badge: true}, {ResourceID: config.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame}, {ResourceID: config.EntryEffectResourceID, PropsType: vipPropsTypeRide}, {ResourceID: config.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble}, {ResourceID: config.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture}, + {ResourceID: config.BackgroundCardResourceID, PropsType: vipPropsTypeDataCard}, }) } @@ -656,10 +676,12 @@ func vipGrantResourcesFromState(state *model.UserVipState) []vipGrantResource { } return compactVIPGrantResources([]vipGrantResource{ {ResourceID: state.BadgeResourceID, Badge: true}, + {ResourceID: state.ShortBadgeResourceID, Badge: true}, {ResourceID: state.AvatarFrameResourceID, PropsType: vipPropsTypeAvatarFrame}, {ResourceID: state.EntryEffectResourceID, PropsType: vipPropsTypeRide}, {ResourceID: state.ChatBubbleResourceID, PropsType: vipPropsTypeChatBubble}, {ResourceID: state.FloatPictureResourceID, PropsType: vipPropsTypeFloatPicture}, + {ResourceID: state.BackgroundCardResourceID, PropsType: vipPropsTypeDataCard}, }) } diff --git a/internal/service/vip/config.go b/internal/service/vip/config.go index 1bffd46..42dc253 100644 --- a/internal/service/vip/config.go +++ b/internal/service/vip/config.go @@ -120,11 +120,15 @@ func normalizeSaveLevels(levels []VipLevelPayload) ([]VipLevelPayload, error) { item.DisplayName = levelDisplayName(item.Level) } item.DurationDays = vipDurationDay - item.Badge = normalizeResource(item.Badge) + item.LongBadge = normalizeResource(firstResource(item.LongBadge, item.Badge)) + item.Badge = item.LongBadge + item.ShortBadge = normalizeResource(item.ShortBadge) item.AvatarFrame = normalizeResource(item.AvatarFrame) item.EntryEffect = normalizeResource(item.EntryEffect) item.ChatBubble = normalizeResource(item.ChatBubble) item.FloatPicture = normalizeResource(item.FloatPicture) + item.BackgroundCard = normalizeResource(item.BackgroundCard) + item.EffectImage = normalizeResource(item.EffectImage) normalized = append(normalized, item) } @@ -159,6 +163,16 @@ func normalizeResource(resource VipResourcePayload) VipResourcePayload { return resource } +func firstResource(preferred VipResourcePayload, fallback VipResourcePayload) VipResourcePayload { + if preferred.ResourceID.Int64() > 0 || strings.TrimSpace(preferred.Name) != "" || + strings.TrimSpace(preferred.URL) != "" || strings.TrimSpace(preferred.SourceURL) != "" || + strings.TrimSpace(preferred.ResourceURL) != "" || strings.TrimSpace(preferred.Cover) != "" || + strings.TrimSpace(preferred.CoverURL) != "" { + return preferred + } + return fallback +} + func applyLevelPayloadToConfig(row *model.VipLevelConfig, item VipLevelPayload, now time.Time) { row.Level = item.Level row.LevelCode = levelCode(item.Level) @@ -166,9 +180,12 @@ func applyLevelPayloadToConfig(row *model.VipLevelConfig, item VipLevelPayload, row.Enabled = item.Enabled row.DurationDays = vipDurationDay row.PriceGold = item.PriceGold - row.BadgeResourceID = item.Badge.ResourceID.Int64() + row.BadgeResourceID = item.LongBadge.ResourceID.Int64() row.BadgeName = "" row.BadgeURL = "" + row.ShortBadgeResourceID = item.ShortBadge.ResourceID.Int64() + row.ShortBadgeName = "" + row.ShortBadgeURL = "" row.AvatarFrameResourceID = item.AvatarFrame.ResourceID.Int64() row.AvatarFrameName = "" row.AvatarFrameURL = "" @@ -181,6 +198,12 @@ func applyLevelPayloadToConfig(row *model.VipLevelConfig, item VipLevelPayload, row.FloatPictureResourceID = item.FloatPicture.ResourceID.Int64() row.FloatPictureName = "" row.FloatPictureURL = "" + row.BackgroundCardResourceID = item.BackgroundCard.ResourceID.Int64() + row.BackgroundCardName = "" + row.BackgroundCardURL = "" + row.EffectImageResourceID = item.EffectImage.ResourceID.Int64() + row.EffectImageName = "" + row.EffectImageURL = "" row.UpdateTime = now if row.CreateTime.IsZero() { row.CreateTime = now diff --git a/internal/service/vip/purchase_test.go b/internal/service/vip/purchase_test.go index 1a25e41..c04d7af 100644 --- a/internal/service/vip/purchase_test.go +++ b/internal/service/vip/purchase_test.go @@ -166,30 +166,46 @@ func TestApplyPlanToStateKeepsOnlyResourceIDsAndPayloadUsesLookup(t *testing.T) ExpireAt: now.AddDate(0, 0, vipDurationDay), DurationDays: vipDurationDay, TargetConfig: model.VipLevelConfig{ - Level: 3, - DisplayName: "VIP 3", - AvatarFrameResourceID: 11, - AvatarFrameName: "frame", - AvatarFrameURL: "https://example.com/frame.svga", - EntryEffectResourceID: 12, - EntryEffectName: "ride", - EntryEffectURL: "https://example.com/ride.svga", - ChatBubbleResourceID: 13, - ChatBubbleName: "bubble", - ChatBubbleURL: "https://example.com/bubble.svga", - FloatPictureResourceID: 14, - FloatPictureName: "float", - FloatPictureURL: "https://example.com/float.svga", + Level: 3, + DisplayName: "VIP 3", + BadgeResourceID: 10, + BadgeName: "long badge", + BadgeURL: "https://example.com/long.png", + ShortBadgeResourceID: 15, + ShortBadgeName: "short badge", + ShortBadgeURL: "https://example.com/short.png", + AvatarFrameResourceID: 11, + AvatarFrameName: "frame", + AvatarFrameURL: "https://example.com/frame.svga", + EntryEffectResourceID: 12, + EntryEffectName: "ride", + EntryEffectURL: "https://example.com/ride.svga", + ChatBubbleResourceID: 13, + ChatBubbleName: "bubble", + ChatBubbleURL: "https://example.com/bubble.svga", + FloatPictureResourceID: 14, + FloatPictureName: "float", + FloatPictureURL: "https://example.com/float.svga", + BackgroundCardResourceID: 16, + BackgroundCardName: "profile card", + BackgroundCardURL: "https://example.com/card.pag", + EffectImageResourceID: 17, + EffectImageName: "effect image", + EffectImageURL: "https://example.com/effect.svga", }, } state := &model.UserVipState{UserID: 1001, SysOrigin: "LIKEI"} applyPlanToState(state, plan, now) - if state.AvatarFrameResourceID != plan.TargetConfig.AvatarFrameResourceID || + if state.BadgeResourceID != plan.TargetConfig.BadgeResourceID || + state.ShortBadgeResourceID != plan.TargetConfig.ShortBadgeResourceID || + state.AvatarFrameResourceID != plan.TargetConfig.AvatarFrameResourceID || state.EntryEffectResourceID != plan.TargetConfig.EntryEffectResourceID || state.ChatBubbleResourceID != plan.TargetConfig.ChatBubbleResourceID || - state.FloatPictureResourceID != plan.TargetConfig.FloatPictureResourceID { + state.FloatPictureResourceID != plan.TargetConfig.FloatPictureResourceID || + state.BackgroundCardResourceID != plan.TargetConfig.BackgroundCardResourceID || + state.EffectImageResourceID != plan.TargetConfig.EffectImageResourceID { t.Fatalf("state resource ids were not copied from target config") } if state.AvatarFrameName != "" || state.AvatarFrameURL != "" { @@ -197,15 +213,24 @@ func TestApplyPlanToStateKeepsOnlyResourceIDsAndPayloadUsesLookup(t *testing.T) } payload := statePayloadFromModelWithResources(state, now, vipResourceLookup{ + 10: {ID: 10, Name: "long badge", Cover: "https://example.com/long.png", SourceURL: "https://example.com/long.svga"}, 11: {ID: 11, Name: "frame", Cover: "https://example.com/frame.png", SourceURL: "https://example.com/frame.svga"}, 12: {ID: 12, Name: "ride", Cover: "https://example.com/ride.png", SourceURL: "https://example.com/ride.svga"}, 13: {ID: 13, Name: "bubble", Cover: "https://example.com/bubble.png", SourceURL: "https://example.com/bubble.svga"}, 14: {ID: 14, Name: "float", Cover: "https://example.com/float.png", SourceURL: "https://example.com/float.svga"}, + 15: {ID: 15, Name: "short badge", Cover: "https://example.com/short.png", SourceURL: "https://example.com/short.svga"}, + 16: {ID: 16, Name: "profile card", Cover: "https://example.com/card.png", SourceURL: "https://example.com/card.pag"}, + 17: {ID: 17, Name: "effect image", Cover: "https://example.com/effect.png", SourceURL: "https://example.com/effect.svga"}, }) - if payload.AvatarFrame.Cover != "https://example.com/frame.png" || + if payload.LongBadge.Cover != "https://example.com/long.png" || + payload.Badge.Cover != "https://example.com/long.png" || + payload.ShortBadge.Cover != "https://example.com/short.png" || + payload.AvatarFrame.Cover != "https://example.com/frame.png" || payload.EntryEffect.Cover != "https://example.com/ride.png" || payload.ChatBubble.Cover != "https://example.com/bubble.png" || - payload.FloatPicture.Cover != "https://example.com/float.png" { + payload.FloatPicture.Cover != "https://example.com/float.png" || + payload.BackgroundCard.Cover != "https://example.com/card.png" || + payload.EffectImage.Cover != "https://example.com/effect.png" { t.Fatalf("state payload covers were not loaded from resource lookup") } } @@ -239,16 +264,19 @@ func TestMapWalletPurchaseErrorGatewayFailure(t *testing.T) { func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) { config := model.VipLevelConfig{ - BadgeResourceID: 10, - AvatarFrameResourceID: 11, - EntryEffectResourceID: 12, - ChatBubbleResourceID: 13, - FloatPictureResourceID: 14, + BadgeResourceID: 10, + ShortBadgeResourceID: 15, + AvatarFrameResourceID: 11, + EntryEffectResourceID: 12, + ChatBubbleResourceID: 13, + FloatPictureResourceID: 14, + BackgroundCardResourceID: 16, + EffectImageResourceID: 17, } resources := vipGrantResourcesFromConfig(config) - if len(resources) != 5 { - t.Fatalf("resources len = %d, want 5", len(resources)) + if len(resources) != 7 { + t.Fatalf("resources len = %d, want 7", len(resources)) } assertGrant := func(index int, resourceID int64, propsType string, badge bool) { t.Helper() @@ -258,10 +286,12 @@ func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) { } } assertGrant(0, 10, "", true) - assertGrant(1, 11, vipPropsTypeAvatarFrame, false) - assertGrant(2, 12, vipPropsTypeRide, false) - assertGrant(3, 13, vipPropsTypeChatBubble, false) - assertGrant(4, 14, vipPropsTypeFloatPicture, false) + assertGrant(1, 15, "", true) + assertGrant(2, 11, vipPropsTypeAvatarFrame, false) + assertGrant(3, 12, vipPropsTypeRide, false) + assertGrant(4, 13, vipPropsTypeChatBubble, false) + assertGrant(5, 14, vipPropsTypeFloatPicture, false) + assertGrant(6, 16, vipPropsTypeDataCard, false) } func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) { diff --git a/internal/service/vip/resources.go b/internal/service/vip/resources.go index df30729..f370e30 100644 --- a/internal/service/vip/resources.go +++ b/internal/service/vip/resources.go @@ -54,7 +54,7 @@ func compactResourceIDs(resourceIDs []int64) []int64 { } func resourceIDsFromConfigRows(rows []model.VipLevelConfig) []int64 { - ids := make([]int64, 0, len(rows)*5) + ids := make([]int64, 0, len(rows)*8) for _, row := range rows { ids = appendResourceIDsFromConfig(ids, row) } @@ -62,7 +62,7 @@ func resourceIDsFromConfigRows(rows []model.VipLevelConfig) []int64 { } func resourceIDsFromConfigMap(configs map[int]model.VipLevelConfig) []int64 { - ids := make([]int64, 0, len(configs)*5) + ids := make([]int64, 0, len(configs)*8) for _, row := range configs { ids = appendResourceIDsFromConfig(ids, row) } @@ -73,10 +73,13 @@ func appendResourceIDsFromConfig(ids []int64, row model.VipLevelConfig) []int64 return append( ids, row.BadgeResourceID, + row.ShortBadgeResourceID, row.AvatarFrameResourceID, row.EntryEffectResourceID, row.ChatBubbleResourceID, row.FloatPictureResourceID, + row.BackgroundCardResourceID, + row.EffectImageResourceID, ) } @@ -87,9 +90,12 @@ func appendResourceIDsFromState(ids []int64, row *model.UserVipState) []int64 { return append( ids, row.BadgeResourceID, + row.ShortBadgeResourceID, row.AvatarFrameResourceID, row.EntryEffectResourceID, row.ChatBubbleResourceID, row.FloatPictureResourceID, + row.BackgroundCardResourceID, + row.EffectImageResourceID, ) } diff --git a/internal/service/vip/types.go b/internal/service/vip/types.go index 265f4f8..695630a 100644 --- a/internal/service/vip/types.go +++ b/internal/service/vip/types.go @@ -40,6 +40,7 @@ const ( vipPropsTypeRide = "RIDE" vipPropsTypeChatBubble = "CHAT_BUBBLE" vipPropsTypeFloatPicture = "FLOAT_PICTURE" + vipPropsTypeDataCard = "DATA_CARD" ) type AppError = common.AppError @@ -82,19 +83,23 @@ type ConfigResponse struct { } type VipLevelPayload struct { - ID int64 `json:"id"` - Level int `json:"level"` - LevelCode string `json:"levelCode"` - DisplayName string `json:"displayName"` - Enabled bool `json:"enabled"` - DurationDays int `json:"durationDays"` - PriceGold int64 `json:"priceGold"` - Badge VipResourcePayload `json:"badge"` - AvatarFrame VipResourcePayload `json:"avatarFrame"` - EntryEffect VipResourcePayload `json:"entryEffect"` - ChatBubble VipResourcePayload `json:"chatBubble"` - FloatPicture VipResourcePayload `json:"floatPicture"` - UpdateTime string `json:"updateTime,omitempty"` + ID int64 `json:"id"` + Level int `json:"level"` + LevelCode string `json:"levelCode"` + DisplayName string `json:"displayName"` + Enabled bool `json:"enabled"` + DurationDays int `json:"durationDays"` + PriceGold int64 `json:"priceGold"` + Badge VipResourcePayload `json:"badge"` + LongBadge VipResourcePayload `json:"longBadge"` + ShortBadge VipResourcePayload `json:"shortBadge"` + AvatarFrame VipResourcePayload `json:"avatarFrame"` + EntryEffect VipResourcePayload `json:"entryEffect"` + ChatBubble VipResourcePayload `json:"chatBubble"` + FloatPicture VipResourcePayload `json:"floatPicture"` + BackgroundCard VipResourcePayload `json:"backgroundCard"` + EffectImage VipResourcePayload `json:"effectImage"` + UpdateTime string `json:"updateTime,omitempty"` } type VipResourcePayload struct { @@ -183,21 +188,25 @@ type VipLevelHomePayload struct { } type UserVipStatePayload struct { - UserID int64 `json:"userId"` - SysOrigin string `json:"sysOrigin"` - Active bool `json:"active"` - Status string `json:"status"` - Level int `json:"level"` - LevelCode string `json:"levelCode"` - DisplayName string `json:"displayName"` - StartAt string `json:"startAt,omitempty"` - ExpireAt string `json:"expireAt,omitempty"` - Badge VipResourcePayload `json:"badge,omitempty"` - AvatarFrame VipResourcePayload `json:"avatarFrame,omitempty"` - EntryEffect VipResourcePayload `json:"entryEffect,omitempty"` - ChatBubble VipResourcePayload `json:"chatBubble,omitempty"` - FloatPicture VipResourcePayload `json:"floatPicture,omitempty"` - UpdateTime string `json:"updateTime,omitempty"` + UserID int64 `json:"userId"` + SysOrigin string `json:"sysOrigin"` + Active bool `json:"active"` + Status string `json:"status"` + Level int `json:"level"` + LevelCode string `json:"levelCode"` + DisplayName string `json:"displayName"` + StartAt string `json:"startAt,omitempty"` + ExpireAt string `json:"expireAt,omitempty"` + Badge VipResourcePayload `json:"badge,omitempty"` + LongBadge VipResourcePayload `json:"longBadge,omitempty"` + ShortBadge VipResourcePayload `json:"shortBadge,omitempty"` + AvatarFrame VipResourcePayload `json:"avatarFrame,omitempty"` + EntryEffect VipResourcePayload `json:"entryEffect,omitempty"` + ChatBubble VipResourcePayload `json:"chatBubble,omitempty"` + FloatPicture VipResourcePayload `json:"floatPicture,omitempty"` + BackgroundCard VipResourcePayload `json:"backgroundCard,omitempty"` + EffectImage VipResourcePayload `json:"effectImage,omitempty"` + UpdateTime string `json:"updateTime,omitempty"` } type VipOrderPayload struct { @@ -356,19 +365,23 @@ func payloadFromConfigWithResources(row model.VipLevelConfig, resources vipResou code = levelCode(row.Level) } return VipLevelPayload{ - ID: row.ID, - Level: row.Level, - LevelCode: code, - DisplayName: displayName, - Enabled: row.Enabled, - DurationDays: duration, - PriceGold: row.PriceGold, - Badge: resourcePayloadWithLookup(row.BadgeResourceID, row.BadgeName, row.BadgeURL, "", resources), - AvatarFrame: resourcePayloadWithLookup(row.AvatarFrameResourceID, row.AvatarFrameName, row.AvatarFrameURL, "", resources), - EntryEffect: resourcePayloadWithLookup(row.EntryEffectResourceID, row.EntryEffectName, row.EntryEffectURL, "", resources), - ChatBubble: resourcePayloadWithLookup(row.ChatBubbleResourceID, row.ChatBubbleName, row.ChatBubbleURL, "", resources), - FloatPicture: resourcePayloadWithLookup(row.FloatPictureResourceID, row.FloatPictureName, row.FloatPictureURL, "", resources), - UpdateTime: formatTime(row.UpdateTime), + ID: row.ID, + Level: row.Level, + LevelCode: code, + DisplayName: displayName, + Enabled: row.Enabled, + DurationDays: duration, + PriceGold: row.PriceGold, + Badge: resourcePayloadWithLookup(row.BadgeResourceID, row.BadgeName, row.BadgeURL, "", resources), + LongBadge: resourcePayloadWithLookup(row.BadgeResourceID, row.BadgeName, row.BadgeURL, "", resources), + ShortBadge: resourcePayloadWithLookup(row.ShortBadgeResourceID, row.ShortBadgeName, row.ShortBadgeURL, "", resources), + AvatarFrame: resourcePayloadWithLookup(row.AvatarFrameResourceID, row.AvatarFrameName, row.AvatarFrameURL, "", resources), + EntryEffect: resourcePayloadWithLookup(row.EntryEffectResourceID, row.EntryEffectName, row.EntryEffectURL, "", resources), + ChatBubble: resourcePayloadWithLookup(row.ChatBubbleResourceID, row.ChatBubbleName, row.ChatBubbleURL, "", resources), + FloatPicture: resourcePayloadWithLookup(row.FloatPictureResourceID, row.FloatPictureName, row.FloatPictureURL, "", resources), + BackgroundCard: resourcePayloadWithLookup(row.BackgroundCardResourceID, row.BackgroundCardName, row.BackgroundCardURL, "", resources), + EffectImage: resourcePayloadWithLookup(row.EffectImageResourceID, row.EffectImageName, row.EffectImageURL, "", resources), + UpdateTime: formatTime(row.UpdateTime), } } @@ -402,10 +415,14 @@ func statePayloadFromModelWithResources(row *model.UserVipState, now time.Time, return payload } payload.Badge = resourcePayloadWithLookup(row.BadgeResourceID, row.BadgeName, row.BadgeURL, "", resources) + payload.LongBadge = payload.Badge + payload.ShortBadge = resourcePayloadWithLookup(row.ShortBadgeResourceID, row.ShortBadgeName, row.ShortBadgeURL, "", resources) payload.AvatarFrame = resourcePayloadWithLookup(row.AvatarFrameResourceID, row.AvatarFrameName, row.AvatarFrameURL, "", resources) payload.EntryEffect = resourcePayloadWithLookup(row.EntryEffectResourceID, row.EntryEffectName, row.EntryEffectURL, "", resources) payload.ChatBubble = resourcePayloadWithLookup(row.ChatBubbleResourceID, row.ChatBubbleName, row.ChatBubbleURL, "", resources) payload.FloatPicture = resourcePayloadWithLookup(row.FloatPictureResourceID, row.FloatPictureName, row.FloatPictureURL, "", resources) + payload.BackgroundCard = resourcePayloadWithLookup(row.BackgroundCardResourceID, row.BackgroundCardName, row.BackgroundCardURL, "", resources) + payload.EffectImage = resourcePayloadWithLookup(row.EffectImageResourceID, row.EffectImageName, row.EffectImageURL, "", resources) return payload } diff --git a/migrations/042_vip_badge_card_effect_resources.sql b/migrations/042_vip_badge_card_effect_resources.sql new file mode 100644 index 0000000..6901fd2 --- /dev/null +++ b/migrations/042_vip_badge_card_effect_resources.sql @@ -0,0 +1,253 @@ +SET @current_schema := DATABASE(); + +SET @add_vip_level_short_badge_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'short_badge_resource_id' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `short_badge_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''短徽章资源ID'' AFTER `badge_url`', + 'SELECT 1' +); +PREPARE add_vip_level_short_badge_resource_id_stmt FROM @add_vip_level_short_badge_resource_id_sql; +EXECUTE add_vip_level_short_badge_resource_id_stmt; +DEALLOCATE PREPARE add_vip_level_short_badge_resource_id_stmt; + +SET @add_vip_level_short_badge_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'short_badge_name' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `short_badge_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''短徽章名称'' AFTER `short_badge_resource_id`', + 'SELECT 1' +); +PREPARE add_vip_level_short_badge_name_stmt FROM @add_vip_level_short_badge_name_sql; +EXECUTE add_vip_level_short_badge_name_stmt; +DEALLOCATE PREPARE add_vip_level_short_badge_name_stmt; + +SET @add_vip_level_short_badge_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'short_badge_url' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `short_badge_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''短徽章资源URL'' AFTER `short_badge_name`', + 'SELECT 1' +); +PREPARE add_vip_level_short_badge_url_stmt FROM @add_vip_level_short_badge_url_sql; +EXECUTE add_vip_level_short_badge_url_stmt; +DEALLOCATE PREPARE add_vip_level_short_badge_url_stmt; + +SET @add_vip_level_background_card_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'background_card_resource_id' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `background_card_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''背景卡资源ID'' AFTER `float_picture_url`', + 'SELECT 1' +); +PREPARE add_vip_level_background_card_resource_id_stmt FROM @add_vip_level_background_card_resource_id_sql; +EXECUTE add_vip_level_background_card_resource_id_stmt; +DEALLOCATE PREPARE add_vip_level_background_card_resource_id_stmt; + +SET @add_vip_level_background_card_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'background_card_name' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `background_card_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''背景卡名称'' AFTER `background_card_resource_id`', + 'SELECT 1' +); +PREPARE add_vip_level_background_card_name_stmt FROM @add_vip_level_background_card_name_sql; +EXECUTE add_vip_level_background_card_name_stmt; +DEALLOCATE PREPARE add_vip_level_background_card_name_stmt; + +SET @add_vip_level_background_card_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'background_card_url' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `background_card_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''背景卡资源URL'' AFTER `background_card_name`', + 'SELECT 1' +); +PREPARE add_vip_level_background_card_url_stmt FROM @add_vip_level_background_card_url_sql; +EXECUTE add_vip_level_background_card_url_stmt; +DEALLOCATE PREPARE add_vip_level_background_card_url_stmt; + +SET @add_vip_level_effect_image_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'effect_image_resource_id' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `effect_image_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''动效图资源ID'' AFTER `background_card_url`', + 'SELECT 1' +); +PREPARE add_vip_level_effect_image_resource_id_stmt FROM @add_vip_level_effect_image_resource_id_sql; +EXECUTE add_vip_level_effect_image_resource_id_stmt; +DEALLOCATE PREPARE add_vip_level_effect_image_resource_id_stmt; + +SET @add_vip_level_effect_image_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'effect_image_name' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `effect_image_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''动效图名称'' AFTER `effect_image_resource_id`', + 'SELECT 1' +); +PREPARE add_vip_level_effect_image_name_stmt FROM @add_vip_level_effect_image_name_sql; +EXECUTE add_vip_level_effect_image_name_stmt; +DEALLOCATE PREPARE add_vip_level_effect_image_name_stmt; + +SET @add_vip_level_effect_image_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'vip_level_config' + AND column_name = 'effect_image_url' + ), + 'ALTER TABLE `vip_level_config` ADD COLUMN `effect_image_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''动效图资源URL'' AFTER `effect_image_name`', + 'SELECT 1' +); +PREPARE add_vip_level_effect_image_url_stmt FROM @add_vip_level_effect_image_url_sql; +EXECUTE add_vip_level_effect_image_url_stmt; +DEALLOCATE PREPARE add_vip_level_effect_image_url_stmt; + +SET @add_user_vip_short_badge_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'short_badge_resource_id' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `short_badge_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''当前短徽章资源ID'' AFTER `badge_url`', + 'SELECT 1' +); +PREPARE add_user_vip_short_badge_resource_id_stmt FROM @add_user_vip_short_badge_resource_id_sql; +EXECUTE add_user_vip_short_badge_resource_id_stmt; +DEALLOCATE PREPARE add_user_vip_short_badge_resource_id_stmt; + +SET @add_user_vip_short_badge_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'short_badge_name' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `short_badge_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''当前短徽章名称'' AFTER `short_badge_resource_id`', + 'SELECT 1' +); +PREPARE add_user_vip_short_badge_name_stmt FROM @add_user_vip_short_badge_name_sql; +EXECUTE add_user_vip_short_badge_name_stmt; +DEALLOCATE PREPARE add_user_vip_short_badge_name_stmt; + +SET @add_user_vip_short_badge_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'short_badge_url' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `short_badge_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''当前短徽章资源URL'' AFTER `short_badge_name`', + 'SELECT 1' +); +PREPARE add_user_vip_short_badge_url_stmt FROM @add_user_vip_short_badge_url_sql; +EXECUTE add_user_vip_short_badge_url_stmt; +DEALLOCATE PREPARE add_user_vip_short_badge_url_stmt; + +SET @add_user_vip_background_card_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'background_card_resource_id' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `background_card_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''当前背景卡资源ID'' AFTER `float_picture_url`', + 'SELECT 1' +); +PREPARE add_user_vip_background_card_resource_id_stmt FROM @add_user_vip_background_card_resource_id_sql; +EXECUTE add_user_vip_background_card_resource_id_stmt; +DEALLOCATE PREPARE add_user_vip_background_card_resource_id_stmt; + +SET @add_user_vip_background_card_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'background_card_name' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `background_card_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''当前背景卡名称'' AFTER `background_card_resource_id`', + 'SELECT 1' +); +PREPARE add_user_vip_background_card_name_stmt FROM @add_user_vip_background_card_name_sql; +EXECUTE add_user_vip_background_card_name_stmt; +DEALLOCATE PREPARE add_user_vip_background_card_name_stmt; + +SET @add_user_vip_background_card_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'background_card_url' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `background_card_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''当前背景卡资源URL'' AFTER `background_card_name`', + 'SELECT 1' +); +PREPARE add_user_vip_background_card_url_stmt FROM @add_user_vip_background_card_url_sql; +EXECUTE add_user_vip_background_card_url_stmt; +DEALLOCATE PREPARE add_user_vip_background_card_url_stmt; + +SET @add_user_vip_effect_image_resource_id_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'effect_image_resource_id' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `effect_image_resource_id` bigint NOT NULL DEFAULT 0 COMMENT ''当前动效图资源ID'' AFTER `background_card_url`', + 'SELECT 1' +); +PREPARE add_user_vip_effect_image_resource_id_stmt FROM @add_user_vip_effect_image_resource_id_sql; +EXECUTE add_user_vip_effect_image_resource_id_stmt; +DEALLOCATE PREPARE add_user_vip_effect_image_resource_id_stmt; + +SET @add_user_vip_effect_image_name_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'effect_image_name' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `effect_image_name` varchar(128) NOT NULL DEFAULT '''' COMMENT ''当前动效图名称'' AFTER `effect_image_resource_id`', + 'SELECT 1' +); +PREPARE add_user_vip_effect_image_name_stmt FROM @add_user_vip_effect_image_name_sql; +EXECUTE add_user_vip_effect_image_name_stmt; +DEALLOCATE PREPARE add_user_vip_effect_image_name_stmt; + +SET @add_user_vip_effect_image_url_sql := IF( + NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = @current_schema + AND table_name = 'user_vip_state' + AND column_name = 'effect_image_url' + ), + 'ALTER TABLE `user_vip_state` ADD COLUMN `effect_image_url` varchar(512) NOT NULL DEFAULT '''' COMMENT ''当前动效图资源URL'' AFTER `effect_image_name`', + 'SELECT 1' +); +PREPARE add_user_vip_effect_image_url_stmt FROM @add_user_vip_effect_image_url_sql; +EXECUTE add_user_vip_effect_image_url_stmt; +DEALLOCATE PREPARE add_user_vip_effect_image_url_stmt;