package rechargereward import ( "context" "errors" "net/http" "sort" "strings" "time" "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "gorm.io/gorm" ) // GetConfig 返回充值奖励后台配置;未配置时返回空档位草稿。 func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*RechargeRewardConfigResponse, error) { sysOrigin = s.normalizeSysOrigin(sysOrigin) var configRow model.RechargeRewardConfig err := s.db.WithContext(ctx). Where("sys_origin = ?", sysOrigin). First(&configRow).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &RechargeRewardConfigResponse{ Configured: false, SysOrigin: sysOrigin, Enabled: false, Timezone: s.defaultTimezone(), LevelConfigs: []RechargeRewardLevelPayload{}, }, nil } if err != nil { return nil, err } levels, err := s.loadLevels(ctx, configRow.ID) if err != nil { return nil, err } rewardItems := s.loadRewardItemsMap(ctx, levels) return &RechargeRewardConfigResponse{ Configured: true, ID: configRow.ID, SysOrigin: configRow.SysOrigin, Enabled: configRow.Enabled, Timezone: normalizeRechargeRewardTimezone(configRow.Timezone), UpdateTime: formatDateTime(configRow.UpdateTime), LevelConfigs: buildAdminLevelPayloads(levels, rewardItems), }, nil } // SaveConfig 保存充值奖励后台配置;同一系统只保留一份当前配置。 func (s *Service) SaveConfig(ctx context.Context, req SaveRechargeRewardConfigRequest) (*RechargeRewardConfigResponse, error) { requestID := req.ID.Int64() sysOrigin := s.normalizeSysOrigin(req.SysOrigin) timezone := normalizeRechargeRewardTimezone(req.Timezone) if _, err := time.LoadLocation(timezone); err != nil { return nil, NewAppError(http.StatusBadRequest, "invalid_timezone", err.Error()) } levels, err := s.normalizeLevelInputs(req.LevelConfigs) if err != nil { return nil, err } if req.Enabled && !hasEnabledLevel(levels) { return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "enabled config must contain at least one enabled level") } var savedConfigID int64 if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() var configRow model.RechargeRewardConfig if requestID > 0 { err := tx.Where("id = ?", requestID).First(&configRow).Error if errors.Is(err, gorm.ErrRecordNotFound) { return NewAppError(http.StatusNotFound, "recharge_reward_config_not_found", "config not found") } if err != nil { return err } if strings.TrimSpace(req.SysOrigin) == "" { sysOrigin = configRow.SysOrigin } } else { err := tx.Where("sys_origin = ?", sysOrigin).First(&configRow).Error if errors.Is(err, gorm.ErrRecordNotFound) { configID, idErr := utils.NextID() if idErr != nil { return idErr } configRow = model.RechargeRewardConfig{ ID: configID, SysOrigin: sysOrigin, CreateTime: now, } } else if err != nil { return err } } configRow.SysOrigin = sysOrigin configRow.Enabled = req.Enabled configRow.Timezone = timezone configRow.UpdateTime = now if configRow.CreateTime.IsZero() { configRow.CreateTime = now } if err := tx.Save(&configRow).Error; err != nil { return err } if err := tx.Where("config_id = ?", configRow.ID).Delete(&model.RechargeRewardLevel{}).Error; err != nil { return err } for index := range levels { levels[index].ConfigID = configRow.ID levels[index].CreateTime = now levels[index].UpdateTime = now } if len(levels) > 0 { if err := tx.Create(&levels).Error; err != nil { return err } } savedConfigID = configRow.ID return nil }); err != nil { return nil, err } return s.GetConfig(ctx, rechargeRewardSysOriginBySavedID(ctx, s.db, savedConfigID, sysOrigin)) } func (s *Service) normalizeLevelInputs(inputs []RechargeRewardLevelInput) ([]model.RechargeRewardLevel, error) { if len(inputs) == 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_level_configs", "levelConfigs is required") } seenLevels := make(map[int]struct{}, len(inputs)) seenAmounts := make(map[int64]struct{}, len(inputs)) rows := make([]model.RechargeRewardLevel, 0, len(inputs)) for _, item := range inputs { if item.Level <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_level", "level must be greater than 0") } amountCents := item.RechargeAmount.Cents() if amountCents == 0 && item.RechargeAmountCents > 0 { amountCents = item.RechargeAmountCents } if amountCents <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_recharge_amount", "rechargeAmount must be greater than 0") } rewardGroupID := item.RewardGroupID.Int64Ptr() if item.RewardGold <= 0 && (rewardGroupID == nil || *rewardGroupID <= 0) { return nil, NewAppError(http.StatusBadRequest, "invalid_reward", "rewardGold or rewardGroupId is required") } if _, exists := seenLevels[item.Level]; exists { return nil, NewAppError(http.StatusBadRequest, "duplicate_level", "levelConfigs must not contain duplicate levels") } if _, exists := seenAmounts[amountCents]; exists { return nil, NewAppError(http.StatusBadRequest, "duplicate_recharge_amount", "levelConfigs must not contain duplicate rechargeAmount") } seenLevels[item.Level] = struct{}{} seenAmounts[amountCents] = struct{}{} id, err := utils.NextID() if err != nil { return nil, err } rows = append(rows, model.RechargeRewardLevel{ ID: id, Level: item.Level, RechargeAmountCents: amountCents, RewardGold: item.RewardGold, RewardGroupID: normalizeRewardGroupID(rewardGroupID), RewardGroupName: strings.TrimSpace(item.RewardGroupName), Enabled: item.Enabled, }) } sort.Slice(rows, func(i, j int) bool { if rows[i].RechargeAmountCents == rows[j].RechargeAmountCents { return rows[i].Level < rows[j].Level } return rows[i].RechargeAmountCents < rows[j].RechargeAmountCents }) return rows, nil } func (s *Service) loadConfigBundle(ctx context.Context, sysOrigin string) (*rechargeRewardConfigBundle, error) { sysOrigin = s.normalizeSysOrigin(sysOrigin) var configRow model.RechargeRewardConfig err := s.db.WithContext(ctx). Where("sys_origin = ?", sysOrigin). First(&configRow).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &rechargeRewardConfigBundle{}, nil } if err != nil { return nil, err } levels, err := s.loadLevels(ctx, configRow.ID) if err != nil { return nil, err } return &rechargeRewardConfigBundle{ Config: refConfigSnapshot(configSnapshotFromModel(configRow)), Levels: levels, }, nil } func (s *Service) loadLevels(ctx context.Context, configID int64) ([]rechargeRewardLevelSnapshot, error) { var rows []model.RechargeRewardLevel if configID <= 0 { return []rechargeRewardLevelSnapshot{}, nil } if err := s.db.WithContext(ctx). Where("config_id = ?", configID). Order("recharge_amount_cents asc"). Order("level asc"). Find(&rows).Error; err != nil { return nil, err } levels := make([]rechargeRewardLevelSnapshot, 0, len(rows)) for _, row := range rows { levels = append(levels, levelSnapshotFromModel(row)) } sortLevels(levels) return levels, nil } func (s *Service) defaultTimezone() string { timezone := strings.TrimSpace(s.cfg.RechargeReward.Timezone) if timezone == "" { timezone = defaultRechargeRewardTimezone } return timezone } func hasEnabledLevel(levels []model.RechargeRewardLevel) bool { for _, level := range levels { if level.Enabled { return true } } return false } func normalizeRewardGroupID(id *int64) *int64 { if id == nil || *id <= 0 { return nil } return id } func refConfigSnapshot(value rechargeRewardConfigSnapshot) *rechargeRewardConfigSnapshot { return &value } func rechargeRewardSysOriginBySavedID(ctx context.Context, db rechargeRewardDB, configID int64, fallback string) string { if configID <= 0 { return fallback } var row model.RechargeRewardConfig if err := db.WithContext(ctx).Select("sys_origin").Where("id = ?", configID).First(&row).Error; err != nil { return fallback } return row.SysOrigin }