2026-05-12 14:08:46 +08:00

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
}