fix: split badges by display type

This commit is contained in:
hy001 2026-05-16 02:32:16 +08:00
parent 8ace95f78f
commit c1a0049b1d
5 changed files with 115 additions and 23 deletions

View File

@ -19,16 +19,16 @@ func (UserBadgeBackpack) TableName() string { return "user_badge_backpack" }
// SysBadgeConfig stores badge metadata from the Java badge system.
type SysBadgeConfig struct {
ID int64 `gorm:"column:id;primaryKey"`
Type string `gorm:"column:type;size:64"`
DisplayType string `gorm:"column:display_type;size:16"`
BadgeLevel int `gorm:"column:badge_level"`
Milestone int64 `gorm:"column:milestone"`
BadgeName string `gorm:"column:badge_name;size:128"`
BadgeKey string `gorm:"column:badge_key;size:128"`
Del bool `gorm:"column:is_del"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
ID int64 `gorm:"column:id;primaryKey"`
Type string `gorm:"column:type;size:64"`
DisplayType string `gorm:"column:display_type;size:16"`
BadgeLevel int `gorm:"column:badge_level"`
Milestone int64 `gorm:"column:milestone"`
BadgeName string `gorm:"column:badge_name;size:128"`
BadgeKey string `gorm:"column:badge_key;size:128"`
Del bool `gorm:"column:is_del"`
CreateTime time.Time `gorm:"column:create_time"`
UpdateTime time.Time `gorm:"column:update_time"`
}
// TableName returns the badge config table name.

View File

@ -279,6 +279,9 @@ func applyVIPGiftConfigToState(state *model.UserVipState, config model.VipLevelC
state.BadgeResourceID = config.BadgeResourceID
state.BadgeName = config.BadgeName
state.BadgeURL = config.BadgeURL
state.ShortBadgeResourceID = config.ShortBadgeResourceID
state.ShortBadgeName = config.ShortBadgeName
state.ShortBadgeURL = config.ShortBadgeURL
state.AvatarFrameResourceID = config.AvatarFrameResourceID
state.AvatarFrameName = config.AvatarFrameName
state.AvatarFrameURL = config.AvatarFrameURL
@ -365,6 +368,7 @@ func (s *Service) alignVIPGiftResourceExpireAt(ctx context.Context, userID int64
func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource {
resources := []vipGiftResource{
{resourceID: config.BadgeResourceID, propsType: propsTypeBadge, badge: true},
{resourceID: config.ShortBadgeResourceID, propsType: propsTypeBadge, badge: true},
{resourceID: config.AvatarFrameResourceID, propsType: propsTypeAvatarFrame},
{resourceID: config.EntryEffectResourceID, propsType: propsTypeRide},
{resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble},

View File

@ -78,7 +78,7 @@ func TestCurrentBadgesSplitLongAndShort(t *testing.T) {
t.Fatalf("badges len = %d, want 3", len(resp.Badges))
}
long := resp.LongBadges[0]
if long.Type != badgeDisplayTypeLong || long.SourceType != badgeSourceTypeVIP || long.BadgeID != "101" || long.LevelCode != "VIP2" {
if long.Type != badgeDisplayTypeLong || long.SourceType != badgeSourceTypeActivity || long.BadgeID != "101" || long.LevelCode != "VIP2" {
t.Fatalf("long badge = %#v", long)
}
if long.Name != "VIP Badge" || long.URL != "https://cdn.example.com/101.png" {
@ -92,6 +92,44 @@ func TestCurrentBadgesSplitLongAndShort(t *testing.T) {
}
}
func TestCurrentBadgesUsesConfiguredDisplayType(t *testing.T) {
db := newUserBadgeTestDB(t)
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
userID := int64(42)
seedBadgeConfigWithDisplay(t, db, 301, badgeSourceTypeActivity, badgeDisplayTypeLong, "Activity Long", "ACTIVITY_LONG", 1)
seedBadgeConfigWithDisplay(t, db, 302, badgeSourceTypeVIP, badgeDisplayTypeShort, "VIP Short", "VIP_SHORT", 1)
for _, badgeID := range []int64{301, 302} {
if err := db.Create(&model.SysBadgePictureConfig{
ID: badgeID + 1000,
BadgeConfigID: badgeID,
SysOrigin: "LIKEI",
SelectURL: "https://cdn.example.com/" + formatID(badgeID) + ".png",
}).Error; err != nil {
t.Fatalf("create picture %d: %v", badgeID, err)
}
}
backpacks := []model.UserBadgeBackpack{
{ID: 4001, UserID: userID, BadgeID: 301, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now.Add(time.Minute)},
{ID: 4002, UserID: userID, BadgeID: 302, ExpireType: "TEMPORARY", ExpireTime: now.Add(2 * time.Hour), UseProps: true, UpdateTime: now},
}
if err := db.Create(&backpacks).Error; err != nil {
t.Fatalf("create backpacks: %v", err)
}
service := NewService(db)
service.now = func() time.Time { return now }
resp, err := service.Current(context.Background(), common.AuthUser{UserID: userID, SysOrigin: "LIKEI"})
if err != nil {
t.Fatalf("Current() error = %v", err)
}
if len(resp.LongBadges) != 1 || resp.LongBadges[0].BadgeID != "301" || resp.LongBadges[0].Type != badgeDisplayTypeLong {
t.Fatalf("long badges = %#v, want configured activity long badge", resp.LongBadges)
}
if len(resp.ShortBadges) != 1 || resp.ShortBadges[0].BadgeID != "302" || resp.ShortBadges[0].Type != badgeDisplayTypeShort {
t.Fatalf("short badges = %#v, want configured vip short badge", resp.ShortBadges)
}
}
func TestCurrentBadgesRejectsInvalidUser(t *testing.T) {
service := NewService(newUserBadgeTestDB(t))
_, err := service.Current(context.Background(), common.AuthUser{SysOrigin: "LIKEI"})
@ -173,15 +211,21 @@ func newUserBadgeTestDB(t *testing.T) *gorm.DB {
}
func seedBadgeConfig(t *testing.T, db *gorm.DB, id int64, sourceType string, name string, key string, level int) {
t.Helper()
seedBadgeConfigWithDisplay(t, db, id, sourceType, "", name, key, level)
}
func seedBadgeConfigWithDisplay(t *testing.T, db *gorm.DB, id int64, sourceType string, displayType string, name string, key string, level int) {
t.Helper()
if err := db.Create(&model.SysBadgeConfig{
ID: id,
Type: sourceType,
BadgeName: name,
BadgeKey: key,
BadgeLevel: level,
Milestone: id * 10,
Del: false,
ID: id,
Type: sourceType,
DisplayType: displayType,
BadgeName: name,
BadgeKey: key,
BadgeLevel: level,
Milestone: id * 10,
Del: false,
}).Error; err != nil {
t.Fatalf("create badge config %d: %v", id, err)
}

View File

@ -16,9 +16,11 @@ const (
badgeDisplayTypeLong = "LONG"
badgeDisplayTypeShort = "SHORT"
badgeSourceTypeVIP = "VIP"
badgeSourceTypeActivity = "ACTIVITY"
badgeSourceTypeAchievement = "ACHIEVEMENT"
badgeSourceTypeVIP = "VIP"
badgeSourceTypeActivity = "ACTIVITY"
badgeSourceTypeAchievement = "ACHIEVEMENT"
badgeSourceTypeHonorAdmin = "HONOR_ADMIN"
badgeSourceTypeHonorActivity = "HONOR_ACTIVITY"
expireTypePermanent = "PERMANENT"
statusActive = "ACTIVE"
@ -56,8 +58,7 @@ type CurrentBadgeListResponse struct {
ShortBadges []CurrentBadgeItem `json:"shortBadges"`
}
// CurrentBadgeItem is one current badge. Type is LONG for VIP badges and SHORT
// for activity or achievement badges.
// CurrentBadgeItem is one current badge. Type is LONG or SHORT by configured display type.
type CurrentBadgeItem struct {
ID string `json:"id"`
BadgeID string `json:"badgeId"`
@ -144,3 +145,24 @@ func firstNonEmpty(values ...string) string {
}
return ""
}
func normalizeBadgeDisplayType(displayType string, sourceType string, fallback string) string {
normalized := strings.ToUpper(strings.TrimSpace(displayType))
if normalized == badgeDisplayTypeLong || normalized == badgeDisplayTypeShort {
return normalized
}
normalizedFallback := strings.ToUpper(strings.TrimSpace(fallback))
if normalizedFallback == badgeDisplayTypeLong || normalizedFallback == badgeDisplayTypeShort {
return normalizedFallback
}
switch strings.ToUpper(strings.TrimSpace(sourceType)) {
case badgeSourceTypeVIP:
return badgeDisplayTypeLong
case badgeSourceTypeActivity, badgeSourceTypeAchievement, badgeSourceTypeHonorAdmin, badgeSourceTypeHonorActivity:
return badgeDisplayTypeShort
default:
return ""
}
}

View File

@ -0,0 +1,22 @@
-- Store whether a badge should render as a long badge or a short badge in App payloads.
SET @column_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sys_badge_config'
AND COLUMN_NAME = 'display_type'
);
SET @ddl := IF(
@column_exists = 0,
'ALTER TABLE sys_badge_config ADD COLUMN display_type varchar(16) NOT NULL DEFAULT '''' COMMENT ''展示类型: LONG长徽章 SHORT短徽章'' AFTER type',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE sys_badge_config
SET display_type = CASE WHEN type = 'VIP' THEN 'LONG' ELSE 'SHORT' END
WHERE display_type IS NULL OR display_type = '';