2026-05-15 19:42:02 +08:00

224 lines
7.6 KiB
Go

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
}
return s.currentForUser(ctx, sysOrigin, user.UserID)
}
// Other returns another user's badge list in the current authenticated sysOrigin.
func (s *Service) Other(ctx context.Context, viewer AuthUser, targetUserID int64) (*CurrentBadgeListResponse, error) {
if s == nil || s.db == nil {
return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable")
}
sysOrigin, err := normalizeUser(viewer)
if err != nil {
return nil, err
}
if targetUserID <= 0 {
return nil, NewAppError(http.StatusBadRequest, "invalid_target_user_id", "target userId is required")
}
return s.currentForUser(ctx, sysOrigin, targetUserID)
}
func (s *Service) currentForUser(ctx context.Context, sysOrigin string, userID int64) (*CurrentBadgeListResponse, error) {
now := s.now()
longBadges, vipBadgeID, err := s.loadLongBadges(ctx, sysOrigin, userID, now)
if err != nil {
return nil, err
}
shortBadges, err := s.loadShortBadges(ctx, sysOrigin, 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: 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)
}