package managercenter import ( "context" "errors" "fmt" "strings" "time" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( vipStatusActive = "ACTIVE" vipStatusSuccess = "SUCCESS" vipOrderTypeGMGift = "GM_GIFT" vipGiftEventPrefix = "MANAGER-VIP-GIFT-" mysqlTimestampMaxYear = 2038 ) type gmVIPGiftRecordRow struct { ID int64 `gorm:"column:id;primaryKey"` UserID int64 `gorm:"column:user_id"` SysOrigin string `gorm:"column:sys_origin"` FromLevel int `gorm:"column:from_level"` VIPLevel int `gorm:"column:vip_level"` LevelCode string `gorm:"column:level_code"` DisplayName string `gorm:"column:display_name"` DurationDays int `gorm:"column:duration_days"` PriceGold int64 `gorm:"column:price_gold"` StartAt time.Time `gorm:"column:start_at"` ExpireAt time.Time `gorm:"column:expire_at"` OrderID int64 `gorm:"column:order_id"` EventID string `gorm:"column:event_id"` Status string `gorm:"column:status"` ApplicantID int64 `gorm:"column:applicant_id"` ApplicantName string `gorm:"column:applicant_name"` Remarks string `gorm:"column:remarks"` ExecutedTime time.Time `gorm:"column:executed_time"` CreateTime time.Time `gorm:"column:create_time"` UpdateTime time.Time `gorm:"column:update_time"` CreateUser int64 `gorm:"column:create_user"` UpdateUser int64 `gorm:"column:update_user"` } func (gmVIPGiftRecordRow) TableName() string { return "gm_vip_gift_record" } type vipGiftResource struct { resourceID int64 propsType string badge bool } func (s *Service) listVIPProps(ctx context.Context, user AuthUser) (*PropsListResponse, error) { sysOrigin := normalizeSysOrigin(user.SysOrigin) var rows []model.VipLevelConfig if err := s.db.WithContext(ctx). Where("sys_origin = ? AND enabled = ?", sysOrigin, true). Order("level ASC"). Find(&rows).Error; err != nil { return nil, serverError("vip_config_query_failed", err.Error()) } records := make([]PropsView, 0, len(rows)) for _, row := range rows { records = append(records, vipPropsViewFromConfig(row)) } return &PropsListResponse{ Type: propsTypeNobleVIP, Records: records, }, nil } func (s *Service) sendVIPGift(ctx context.Context, manager AuthUser, target userBaseInfoRow, configID int64) (*SendPropsResponse, error) { sysOrigin := normalizeSysOrigin(target.OriginSys) config, found, err := s.loadVIPConfigByID(ctx, sysOrigin, configID) if err != nil || !found { return nil, err } if !config.Enabled { return nil, badRequest("vip_level_disabled", "vip level is disabled") } if config.Level < 1 || config.Level > 5 { return nil, badRequest("vip_not_giftable", "vip level is invalid") } if config.DurationDays <= 0 { config.DurationDays = defaultVIPGiftDays } now := time.Now() orderID, err := utils.NextID() if err != nil { return nil, serverError("id_generate_failed", err.Error()) } recordID, err := utils.NextID() if err != nil { return nil, serverError("id_generate_failed", err.Error()) } stateID, err := utils.NextID() if err != nil { return nil, serverError("id_generate_failed", err.Error()) } eventID := fmt.Sprintf("%s%d", vipGiftEventPrefix, orderID) applicantName := s.managerApplicantName(ctx, manager.UserID) tx := s.db.WithContext(ctx).Begin() if tx.Error != nil { return nil, serverError("vip_gift_failed", tx.Error.Error()) } committed := false defer func() { if !committed { _ = tx.Rollback().Error } }() state, err := s.loadLockedVIPState(tx, sysOrigin, target.ID) if err != nil { return nil, serverError("vip_state_query_failed", err.Error()) } currentActive := isActiveVIPState(state, now) fromLevel := 0 if currentActive { fromLevel = state.Level } if currentActive && fromLevel > config.Level { return nil, badRequest("vip_downgrade_not_allowed", "vip downgrade is not allowed") } startAt := now if currentActive && !state.StartAt.IsZero() { startAt = state.StartAt } expireAt := resolveVIPGiftExpireAt(state, config, currentActive, now) order := model.VipOrderRecord{ ID: orderID, EventID: eventID, SysOrigin: sysOrigin, UserID: target.ID, OrderType: vipOrderTypeGMGift, FromLevel: fromLevel, ToLevel: config.Level, DurationDays: config.DurationDays, OriginalPriceGold: config.PriceGold, PaidGold: 0, Status: vipStatusSuccess, StartAt: startAt, ExpireAt: expireAt, ErrorMessage: "", CreateTime: now, UpdateTime: now, } if err := tx.Create(&order).Error; err != nil { return nil, serverError("vip_order_create_failed", err.Error()) } if state == nil { state = &model.UserVipState{ ID: stateID, SysOrigin: sysOrigin, UserID: target.ID, CreateTime: now, } } applyVIPGiftConfigToState(state, config, startAt, expireAt, now) if err := tx.Save(state).Error; err != nil { return nil, serverError("vip_state_save_failed", err.Error()) } record := gmVIPGiftRecordRow{ ID: recordID, UserID: target.ID, SysOrigin: sysOrigin, FromLevel: fromLevel, VIPLevel: config.Level, LevelCode: vipLevelCode(config), DisplayName: vipDisplayName(config), DurationDays: config.DurationDays, PriceGold: config.PriceGold, StartAt: startAt, ExpireAt: expireAt, OrderID: orderID, EventID: eventID, Status: vipStatusSuccess, ApplicantID: manager.UserID, ApplicantName: applicantName, Remarks: fmt.Sprintf("manager center gift, manager=%d", manager.UserID), ExecutedTime: now, CreateTime: now, UpdateTime: now, CreateUser: manager.UserID, UpdateUser: manager.UserID, } if err := tx.Create(&record).Error; err != nil { return nil, serverError("vip_gift_record_create_failed", err.Error()) } if err := tx.Commit().Error; err != nil { return nil, serverError("vip_gift_failed", err.Error()) } committed = true if err := s.grantVIPGiftResources(ctx, target.ID, manager.UserID, config, expireAt, now); err != nil { return nil, serverError("vip_resource_grant_failed", err.Error()) } return &SendPropsResponse{ Success: true, AcceptUserID: ID(target.ID), PropsID: ID(config.ID), Type: propsTypeNobleVIP, Days: config.DurationDays, VIPLevel: config.Level, }, nil } func (s *Service) loadVIPConfigByID(ctx context.Context, sysOrigin string, configID int64) (model.VipLevelConfig, bool, error) { var row model.VipLevelConfig err := s.db.WithContext(ctx). Where("id = ? AND sys_origin = ?", configID, sysOrigin). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return model.VipLevelConfig{}, false, nil } if err != nil { return model.VipLevelConfig{}, false, serverError("vip_config_query_failed", err.Error()) } return row, true, nil } func (s *Service) loadLockedVIPState(tx *gorm.DB, sysOrigin string, userID int64) (*model.UserVipState, error) { var row model.UserVipState err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("sys_origin = ? AND user_id = ?", sysOrigin, userID). First(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } return &row, nil } func isActiveVIPState(state *model.UserVipState, now time.Time) bool { return state != nil && state.Status == vipStatusActive && state.Level > 0 && state.ExpireAt.After(now) } func resolveVIPGiftExpireAt(state *model.UserVipState, config model.VipLevelConfig, currentActive bool, now time.Time) time.Time { durationDays := config.DurationDays if durationDays <= 0 { durationDays = defaultVIPGiftDays } plusDuration := now.AddDate(0, 0, durationDays) if !currentActive { return plusDuration } if state.Level == config.Level { return maxTime(state.ExpireAt, now).AddDate(0, 0, durationDays) } return maxTime(state.ExpireAt, plusDuration) } func applyVIPGiftConfigToState(state *model.UserVipState, config model.VipLevelConfig, startAt time.Time, expireAt time.Time, now time.Time) { state.Level = config.Level state.LevelCode = vipLevelCode(config) state.DisplayName = vipDisplayName(config) state.Status = vipStatusActive state.StartAt = startAt state.ExpireAt = expireAt state.BadgeResourceID = config.BadgeResourceID state.BadgeName = config.BadgeName state.BadgeURL = config.BadgeURL state.AvatarFrameResourceID = config.AvatarFrameResourceID state.AvatarFrameName = config.AvatarFrameName state.AvatarFrameURL = config.AvatarFrameURL state.EntryEffectResourceID = config.EntryEffectResourceID state.EntryEffectName = config.EntryEffectName state.EntryEffectURL = config.EntryEffectURL state.ChatBubbleResourceID = config.ChatBubbleResourceID state.ChatBubbleName = config.ChatBubbleName state.ChatBubbleURL = config.ChatBubbleURL state.FloatPictureResourceID = config.FloatPictureResourceID state.FloatPictureName = config.FloatPictureName state.FloatPictureURL = config.FloatPictureURL state.UpdateTime = now if state.CreateTime.IsZero() { state.CreateTime = now } } func (s *Service) grantVIPGiftResources(ctx context.Context, userID int64, managerID int64, config model.VipLevelConfig, expireAt time.Time, now time.Time) error { resources := vipGiftResourcesFromConfig(config) if len(resources) == 0 { return nil } days := config.DurationDays if days <= 0 { days = defaultVIPGiftDays } for _, resource := range resources { if resource.badge { if err := s.java.ActivateTemporaryBadge(ctx, userID, resource.resourceID, days); err != nil { return fmt.Errorf("grant vip badge %d: %w", resource.resourceID, err) } } else { if err := s.java.GivePropsBackpack(ctx, integration.GivePropsBackpackRequest{ AcceptUserID: userID, PropsID: resource.resourceID, Type: resource.propsType, Origin: vipGiftOrigin, OriginDesc: vipGiftOriginDesc, Days: days, UseProps: true, AllowGive: false, OpUser: managerID, }); err != nil { return fmt.Errorf("grant vip resource %s:%d: %w", resource.propsType, resource.resourceID, err) } if err := s.java.SwitchUseProps(ctx, userID, resource.resourceID); err != nil { return fmt.Errorf("switch vip resource %s:%d: %w", resource.propsType, resource.resourceID, err) } } if err := s.alignVIPGiftResourceExpireAt(ctx, userID, resource, expireAt, now); err != nil { return err } } if err := s.java.RemoveUserProfileCacheAll(ctx, userID); err != nil { return fmt.Errorf("remove vip user profile cache: %w", err) } return nil } func (s *Service) alignVIPGiftResourceExpireAt(ctx context.Context, userID int64, resource vipGiftResource, expireAt time.Time, now time.Time) error { expireAt = capMySQLTimestamp(expireAt) var result *gorm.DB if resource.badge { result = s.db.WithContext(ctx).Exec( "UPDATE user_badge_backpack SET expire_type = ?, expire_time = ?, is_use_props = ?, update_time = ? WHERE user_id = ? AND badge_id = ?", "TEMPORARY", expireAt, true, now, userID, resource.resourceID, ) } else { result = s.db.WithContext(ctx).Exec( "UPDATE user_props_backpack SET expire_time = ?, is_use_props = ?, allow_give = ?, update_time = ? WHERE user_id = ? AND props_id = ? AND type = ?", expireAt, true, false, now, userID, resource.resourceID, resource.propsType, ) } if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return fmt.Errorf("vip resource backpack row not found after grant: %d", resource.resourceID) } return nil } func vipGiftResourcesFromConfig(config model.VipLevelConfig) []vipGiftResource { resources := []vipGiftResource{ {resourceID: config.BadgeResourceID, propsType: propsTypeBadge, badge: true}, {resourceID: config.AvatarFrameResourceID, propsType: propsTypeAvatarFrame}, {resourceID: config.EntryEffectResourceID, propsType: propsTypeRide}, {resourceID: config.ChatBubbleResourceID, propsType: propsTypeChatBubble}, {resourceID: config.FloatPictureResourceID, propsType: propsTypeFloatPicture}, } out := make([]vipGiftResource, 0, len(resources)) for _, resource := range resources { if resource.resourceID > 0 { out = append(out, resource) } } return out } func (s *Service) managerApplicantName(ctx context.Context, managerID int64) string { row, err := s.loadUserRowByID(ctx, managerID) if err != nil { return fmt.Sprintf("%d", managerID) } if strings.TrimSpace(row.UserNickname) != "" { return row.UserNickname } if strings.TrimSpace(row.Account) != "" { return row.Account } return fmt.Sprintf("%d", managerID) } func vipLevelCode(config model.VipLevelConfig) string { code := strings.TrimSpace(config.LevelCode) if code == "" { return fmt.Sprintf("VIP%d", config.Level) } return code } func vipDisplayName(config model.VipLevelConfig) string { name := strings.TrimSpace(config.DisplayName) if name == "" { return fmt.Sprintf("VIP %d", config.Level) } return name } func maxTime(a time.Time, b time.Time) time.Time { if a.After(b) { return a } return b } func capMySQLTimestamp(value time.Time) time.Time { maxValue := time.Date(mysqlTimestampMaxYear, time.January, 19, 0, 0, 0, 0, time.Local) if value.After(maxValue) { return maxValue } return value }