221 lines
6.3 KiB
Go
221 lines
6.3 KiB
Go
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
|
|
}
|