package binancerecharge import ( "context" "errors" "math/big" "net/http" "sort" "strings" "time" "chatapp3-golang/internal/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (s *Service) GetConfig(ctx context.Context, sysOrigin string) (*ConfigResponse, error) { sysOrigin = normalizeSysOrigin(sysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required") } if s.db == nil { return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable") } var cfg model.BinanceRechargeConfig err := s.db.WithContext(ctx). Where("sys_origin = ?", sysOrigin). First(&cfg).Error if errors.Is(err, gorm.ErrRecordNotFound) { return &ConfigResponse{ SysOrigin: sysOrigin, Enabled: true, Rates: []RateConfigResponse{}, }, nil } if err != nil { return nil, NewAppError(http.StatusInternalServerError, "config_query_failed", err.Error()) } rates, err := s.listRates(ctx, cfg.ID) if err != nil { return nil, err } return &ConfigResponse{ SysOrigin: cfg.SysOrigin, APIKey: cfg.APIKey, APISecretConfigured: strings.TrimSpace(cfg.APISecret) != "", Enabled: cfg.Enabled, Rates: rates, }, nil } func (s *Service) SaveConfig(ctx context.Context, req SaveConfigRequest) (*ConfigResponse, error) { sysOrigin := normalizeSysOrigin(req.SysOrigin) if sysOrigin == "" { return nil, NewAppError(http.StatusBadRequest, "sys_origin_required", "sysOrigin is required") } if s.db == nil { return nil, NewAppError(http.StatusServiceUnavailable, "database_unavailable", "database is unavailable") } apiKey := strings.TrimSpace(req.APIKey) apiSecret := strings.TrimSpace(req.APISecret) rates, err := validateRateRequests(req.Rates) if err != nil { return nil, err } var cfg model.BinanceRechargeConfig err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() var existing model.BinanceRechargeConfig err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("sys_origin = ?", sysOrigin). First(&existing).Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } if apiSecret == "" && existing.ID > 0 { apiSecret = existing.APISecret } if apiKey == "" { return NewAppError(http.StatusBadRequest, "api_key_required", "apiKey is required") } if apiSecret == "" { return NewAppError(http.StatusBadRequest, "api_secret_required", "apiSecret is required") } cfg = model.BinanceRechargeConfig{ ID: existing.ID, SysOrigin: sysOrigin, APIKey: apiKey, APISecret: apiSecret, Enabled: req.Enabled, CreateTime: now, UpdateTime: now, } if existing.ID > 0 { cfg.CreateTime = existing.CreateTime if err := tx.Model(&model.BinanceRechargeConfig{}). Where("id = ?", existing.ID). Updates(map[string]any{ "api_key": apiKey, "api_secret": apiSecret, "enabled": req.Enabled, "update_time": now, }).Error; err != nil { return err } } else if err := tx.Create(&cfg).Error; err != nil { return err } if err := tx.Where("config_id = ?", cfg.ID).Delete(&model.BinanceRechargeRate{}).Error; err != nil { return err } rows := make([]model.BinanceRechargeRate, 0, len(rates)) for index, item := range rates { rows = append(rows, model.BinanceRechargeRate{ ConfigID: cfg.ID, MinAmount: item.MinAmount, MaxAmount: item.MaxAmount, GoldPerUSD: item.GoldPerUSD, SortOrder: index + 1, CreateTime: now, UpdateTime: now, }) } return tx.Create(&rows).Error }) if err != nil { var appErr *AppError if errors.As(err, &appErr) { return nil, err } return nil, NewAppError(http.StatusInternalServerError, "config_save_failed", err.Error()) } return s.GetConfig(ctx, sysOrigin) } func (s *Service) listRates(ctx context.Context, configID int64) ([]RateConfigResponse, error) { var rows []model.BinanceRechargeRate if err := s.db.WithContext(ctx). Where("config_id = ?", configID). Order("sort_order asc, min_amount asc, id asc"). Find(&rows).Error; err != nil { return nil, NewAppError(http.StatusInternalServerError, "rate_query_failed", err.Error()) } resp := make([]RateConfigResponse, 0, len(rows)) for _, row := range rows { resp = append(resp, RateConfigResponse{ ID: row.ID, MinAmount: trimDecimalZeros(row.MinAmount), MaxAmount: trimDecimalZeros(row.MaxAmount), GoldPerUSD: trimDecimalZeros(row.GoldPerUSD), }) } return resp, nil } func validateRateRequests(input []RateConfigRequest) ([]RateConfigResponse, error) { if len(input) == 0 { return nil, NewAppError(http.StatusBadRequest, "rates_required", "at least one rate range is required") } rates := make([]RateConfigResponse, 0, len(input)) for _, item := range input { minRat, minAmount, err := parseNonNegativeDecimal(item.MinAmount, "minAmount") if err != nil { return nil, err } maxRat, maxAmount, err := parseNonNegativeDecimal(defaultZero(item.MaxAmount), "maxAmount") if err != nil { return nil, err } _, goldPerUSD, err := parsePositiveDecimal(item.GoldPerUSD, "goldPerUsd") if err != nil { return nil, err } if maxRat.Sign() > 0 && maxRat.Cmp(minRat) <= 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "maxAmount must be greater than minAmount") } rates = append(rates, RateConfigResponse{ MinAmount: minAmount, MaxAmount: maxAmount, GoldPerUSD: goldPerUSD, }) } sort.SliceStable(rates, func(i, j int) bool { left, _ := new(big.Rat).SetString(rates[i].MinAmount) right, _ := new(big.Rat).SetString(rates[j].MinAmount) return left.Cmp(right) < 0 }) for i := 0; i < len(rates); i++ { currentMax, _ := new(big.Rat).SetString(rates[i].MaxAmount) if currentMax.Sign() == 0 { if i != len(rates)-1 { return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "open-ended rate range must be the last range") } continue } if i+1 < len(rates) { nextMin, _ := new(big.Rat).SetString(rates[i+1].MinAmount) if currentMax.Cmp(nextMin) > 0 { return nil, NewAppError(http.StatusBadRequest, "invalid_rate_range", "rate ranges cannot overlap") } } } return rates, nil } func defaultZero(value string) string { if strings.TrimSpace(value) == "" { return "0" } return value }