feat: add VIP badge card resources

This commit is contained in:
hy001 2026-05-16 02:21:43 +08:00
parent 6a0ae4793b
commit 0d8e6f599b
8 changed files with 524 additions and 146 deletions

View File

@ -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.

View File

@ -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{}).

View File

@ -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},
})
}

View File

@ -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

View File

@ -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) {

View File

@ -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,
)
}

View File

@ -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
}

View File

@ -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;