package userbadge import ( "context" "database/sql" "errors" "net/http" "strings" "time" "chatapp3-golang/internal/model" "gorm.io/gorm" ) type badgePictureRow struct { BadgeID int64 `gorm:"column:badge_id"` SourceType sql.NullString `gorm:"column:source_type"` BadgeLevel sql.NullInt64 `gorm:"column:badge_level"` Milestone sql.NullInt64 `gorm:"column:milestone"` BadgeName sql.NullString `gorm:"column:badge_name"` BadgeKey sql.NullString `gorm:"column:badge_key"` SelectURL sql.NullString `gorm:"column:select_url"` NotSelectURL sql.NullString `gorm:"column:not_select_url"` AnimationURL sql.NullString `gorm:"column:animation_url"` ExpireTime time.Time `gorm:"column:expire_time"` } // Current returns the current user's badge list split by long and short display type. func (s *Service) Current(ctx context.Context, user AuthUser) (*CurrentBadgeListResponse, error) { if s == nil || s.db == nil { return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable") } sysOrigin, err := normalizeUser(user) if err != nil { return nil, err } now := s.now() longBadges, vipBadgeID, err := s.loadLongBadges(ctx, sysOrigin, user.UserID, now) if err != nil { return nil, err } shortBadges, err := s.loadShortBadges(ctx, sysOrigin, user.UserID, vipBadgeID, now) if err != nil { return nil, err } badges := make([]CurrentBadgeItem, 0, len(longBadges)+len(shortBadges)) badges = append(badges, longBadges...) badges = append(badges, shortBadges...) return &CurrentBadgeListResponse{ UserID: user.UserID, SysOrigin: sysOrigin, Badges: badges, LongBadges: longBadges, ShortBadges: shortBadges, }, nil } func (s *Service) loadLongBadges(ctx context.Context, sysOrigin string, userID int64, now time.Time) ([]CurrentBadgeItem, int64, error) { var state model.UserVipState err := s.db.WithContext(ctx). Where("sys_origin = ? AND user_id = ?", sysOrigin, userID). First(&state).Error if errors.Is(err, gorm.ErrRecordNotFound) { return []CurrentBadgeItem{}, 0, nil } if err != nil { return nil, 0, err } if !isActiveVIPState(state, now) || state.BadgeResourceID <= 0 { return []CurrentBadgeItem{}, 0, nil } row, err := s.loadBadgePicture(ctx, sysOrigin, state.BadgeResourceID) if err != nil { return nil, 0, err } item := CurrentBadgeItem{ ID: formatID(state.BadgeResourceID), BadgeID: formatID(state.BadgeResourceID), Type: badgeDisplayTypeLong, SourceType: badgeSourceTypeVIP, BadgeLevel: nullInt(row.BadgeLevel), BadgeKey: nullString(row.BadgeKey), Level: state.Level, LevelCode: state.LevelCode, DisplayName: state.DisplayName, Use: true, ExpireTime: formatMillis(state.ExpireAt), ExpireAt: formatTime(state.ExpireAt), } item.BadgeName = firstNonEmpty(nullString(row.BadgeName), state.BadgeName, state.DisplayName) item.Name = item.BadgeName item.SelectURL = firstNonEmpty(nullString(row.SelectURL), state.BadgeURL) item.NotSelectURL = nullString(row.NotSelectURL) item.AnimationURL = nullString(row.AnimationURL) item.URL = firstNonEmpty(item.SelectURL, state.BadgeURL) if row.Milestone.Valid && row.Milestone.Int64 > 0 { item.Milestone = formatID(row.Milestone.Int64) } if item.Name == "" && item.URL == "" { return []CurrentBadgeItem{}, state.BadgeResourceID, nil } return []CurrentBadgeItem{item}, state.BadgeResourceID, nil } func (s *Service) loadShortBadges(ctx context.Context, sysOrigin string, userID int64, excludeBadgeID int64, now time.Time) ([]CurrentBadgeItem, error) { rows := make([]badgePictureRow, 0) query := s.db.WithContext(ctx). Table("user_badge_backpack AS backpack"). Select(` backpack.badge_id AS badge_id, backpack.expire_time AS expire_time, config.type AS source_type, config.badge_level AS badge_level, config.milestone AS milestone, config.badge_name AS badge_name, config.badge_key AS badge_key, picture.select_url AS select_url, picture.not_select_url AS not_select_url, picture.animation_url AS animation_url `). Joins("JOIN sys_badge_config AS config ON config.id = backpack.badge_id"). Joins("JOIN sys_badge_picture_config AS picture ON picture.badge_config_id = config.id AND picture.sys_origin = ?", sysOrigin). Where("backpack.user_id = ?", userID). Where("backpack.is_use_props = ?", true). Where("config.is_del = ?", false). Where("config.type IN ?", []string{badgeSourceTypeActivity, badgeSourceTypeAchievement}). Where("(backpack.expire_type = ? OR backpack.expire_time > ?)", expireTypePermanent, now). Order("backpack.update_time DESC, backpack.id DESC") if excludeBadgeID > 0 { query = query.Where("backpack.badge_id <> ?", excludeBadgeID) } if err := query.Scan(&rows).Error; err != nil { return nil, err } items := make([]CurrentBadgeItem, 0, len(rows)) for _, row := range rows { item := CurrentBadgeItem{ ID: formatID(row.BadgeID), BadgeID: formatID(row.BadgeID), Type: badgeDisplayTypeShort, SourceType: nullString(row.SourceType), Name: nullString(row.BadgeName), BadgeName: nullString(row.BadgeName), BadgeKey: nullString(row.BadgeKey), BadgeLevel: nullInt(row.BadgeLevel), URL: nullString(row.SelectURL), SelectURL: nullString(row.SelectURL), NotSelectURL: nullString(row.NotSelectURL), AnimationURL: nullString(row.AnimationURL), Use: true, ExpireTime: formatMillis(row.ExpireTime), ExpireAt: formatTime(row.ExpireTime), } if row.Milestone.Valid && row.Milestone.Int64 > 0 { item.Milestone = formatID(row.Milestone.Int64) } items = append(items, item) } return items, nil } func (s *Service) loadBadgePicture(ctx context.Context, sysOrigin string, badgeID int64) (badgePictureRow, error) { var row badgePictureRow err := s.db.WithContext(ctx). Table("sys_badge_config AS config"). Select(` config.id AS badge_id, config.type AS source_type, config.badge_level AS badge_level, config.milestone AS milestone, config.badge_name AS badge_name, config.badge_key AS badge_key, picture.select_url AS select_url, picture.not_select_url AS not_select_url, picture.animation_url AS animation_url `). Joins("LEFT JOIN sys_badge_picture_config AS picture ON picture.badge_config_id = config.id AND picture.sys_origin = ?", sysOrigin). Where("config.id = ? AND config.is_del = ?", badgeID, false). Scan(&row).Error return row, err } func isActiveVIPState(state model.UserVipState, now time.Time) bool { return strings.EqualFold(strings.TrimSpace(state.Status), statusActive) && state.Level > 0 && state.ExpireAt.After(now) } func nullString(value sql.NullString) string { if !value.Valid { return "" } return strings.TrimSpace(value.String) } func nullInt(value sql.NullInt64) int { if !value.Valid { return 0 } return int(value.Int64) }