Merge upstream/master into master

This commit is contained in:
EUForest 2026-02-13 23:06:43 +08:00
commit 31e75efacb
14 changed files with 131 additions and 32 deletions

View File

@ -14,14 +14,14 @@ type (
CreateOrderRequest { CreateOrderRequest {
UserId int64 `json:"user_id" validate:"required"` UserId int64 `json:"user_id" validate:"required"`
Type uint8 `json:"type" validate:"required"` Type uint8 `json:"type" validate:"required"`
Quantity int64 `json:"quantity,omitempty"` Quantity int64 `json:"quantity,omitempty" validate:"omitempty,lte=1000"`
Price int64 `json:"price" validate:"required"` Price int64 `json:"price" validate:"required,gte=0,lte=2000000000"`
Amount int64 `json:"amount" validate:"required"` Amount int64 `json:"amount" validate:"required,gte=0,lte=2147483647"`
Discount int64 `json:"discount,omitempty"` Discount int64 `json:"discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
CouponDiscount int64 `json:"coupon_discount,omitempty"` CouponDiscount int64 `json:"coupon_discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Commission int64 `json:"commission"` Commission int64 `json:"commission" validate:"gte=0,lte=2000000000"`
FeeAmount int64 `json:"fee_amount" validate:"required"` FeeAmount int64 `json:"fee_amount" validate:"required,gte=0,lte=2000000000"`
PaymentId int64 `json:"payment_id" validate:"required"` PaymentId int64 `json:"payment_id" validate:"required"`
TradeNo string `json:"trade_no,omitempty"` TradeNo string `json:"trade_no,omitempty"`
Status uint8 `json:"status,omitempty"` Status uint8 `json:"status,omitempty"`

View File

@ -604,7 +604,7 @@ type (
//public order //public order
PurchaseOrderRequest { PurchaseOrderRequest {
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Quantity int64 `json:"quantity" validate:"required,gt=0"` Quantity int64 `json:"quantity" validate:"required,gt=0,lte=1000"`
Payment int64 `json:"payment,omitempty"` Payment int64 `json:"payment,omitempty"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
} }
@ -622,7 +622,7 @@ type (
} }
RenewalOrderRequest { RenewalOrderRequest {
UserSubscribeID int64 `json:"user_subscribe_id"` UserSubscribeID int64 `json:"user_subscribe_id"`
Quantity int64 `json:"quantity"` Quantity int64 `json:"quantity" validate:"lte=1000"`
Payment int64 `json:"payment"` Payment int64 `json:"payment"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
} }
@ -637,7 +637,7 @@ type (
OrderNo string `json:"order_no"` OrderNo string `json:"order_no"`
} }
RechargeOrderRequest { RechargeOrderRequest {
Amount int64 `json:"amount"` Amount int64 `json:"amount" validate:"required,gt=0,lte=2000000000"`
Payment int64 `json:"payment"` Payment int64 `json:"payment"`
} }
RechargeOrderResponse { RechargeOrderResponse {

View File

@ -24,7 +24,7 @@ func Currency(ctx *svc.ServiceContext) {
AccessKey string AccessKey string
}{} }{}
tool.SystemConfigSliceReflectToStruct(currency, &configs) tool.SystemConfigSliceReflectToStruct(currency, &configs)
ctx.ExchangeRate = 0 // Default exchange rate to 0
ctx.Config.Currency = config.Currency{ ctx.Config.Currency = config.Currency{
Unit: configs.CurrencyUnit, Unit: configs.CurrencyUnit,
Symbol: configs.CurrencySymbol, Symbol: configs.CurrencySymbol,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"reflect" "reflect"
"github.com/perfect-panel/server/initialize"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/system" "github.com/perfect-panel/server/internal/model/system"
"github.com/perfect-panel/server/pkg/tool" "github.com/perfect-panel/server/pkg/tool"
@ -54,6 +55,7 @@ func (l *UpdateCurrencyConfigLogic) UpdateCurrencyConfig(req *types.CurrencyConf
// clear cache // clear cache
return l.svcCtx.Redis.Del(l.ctx, config.CurrencyConfigKey, config.GlobalConfigKey).Err() return l.svcCtx.Redis.Del(l.ctx, config.CurrencyConfigKey, config.GlobalConfigKey).Err()
}) })
initialize.Currency(l.svcCtx)
if err != nil { if err != nil {
l.Errorw("[UpdateCurrencyConfig] update currency config error", logger.Field("error", err.Error())) l.Errorw("[UpdateCurrencyConfig] update currency config error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update invite config error: %v", err) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update invite config error: %v", err)

View File

@ -6,4 +6,9 @@ const (
StripeAlipay = "stripe_alipay" StripeAlipay = "stripe_alipay"
StripeWeChatPay = "stripe_wechat_pay" StripeWeChatPay = "stripe_wechat_pay"
Balance = "balance" Balance = "balance"
// MaxOrderAmount Order amount limits
MaxOrderAmount = 2147483647 // int32 max value (2.1 billion)
MaxRechargeAmount = 2000000000 // 2 billion, slightly lower for safety
MaxQuantity = 1000 // Maximum quantity per order
) )

View File

@ -58,6 +58,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
req.Quantity = 1 req.Quantity = 1
} }
// Validate quantity limit
if req.Quantity > MaxQuantity {
l.Errorw("[Purchase] Quantity exceeds maximum limit", logger.Field("quantity", req.Quantity), logger.Field("max", MaxQuantity))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "quantity exceeds maximum limit of %d", MaxQuantity)
}
// find user subscription // find user subscription
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
if err != nil { if err != nil {
@ -97,6 +103,17 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
// discount amount // discount amount
amount := int64(float64(price) * discount) amount := int64(float64(price) * discount)
discountAmount := price - amount discountAmount := price - amount
// Validate amount to prevent overflow
if amount > MaxOrderAmount {
l.Errorw("[Purchase] Order amount exceeds maximum limit",
logger.Field("amount", amount),
logger.Field("max", MaxOrderAmount),
logger.Field("user_id", u.Id),
logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
var coupon int64 = 0 var coupon int64 = 0
// Calculate the coupon deduction // Calculate the coupon deduction
if req.Coupon != "" { if req.Coupon != "" {
@ -141,6 +158,15 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
if amount > 0 { if amount > 0 {
feeAmount = calculateFee(amount, payment) feeAmount = calculateFee(amount, payment)
amount += feeAmount amount += feeAmount
// Final validation after adding fee
if amount > MaxOrderAmount {
l.Errorw("[Purchase] Final order amount exceeds maximum limit after fee",
logger.Field("amount", amount),
logger.Field("max", MaxOrderAmount),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
} }
// Calculate gift amount deduction after fee calculation // Calculate gift amount deduction after fee calculation
var deductionAmount int64 var deductionAmount int64

View File

@ -40,6 +40,21 @@ func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.R
logger.Error("current user is not found in context") logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
} }
// Validate recharge amount
if req.Amount <= 0 {
l.Errorw("[Recharge] Invalid recharge amount", logger.Field("amount", req.Amount), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "recharge amount must be greater than 0")
}
if req.Amount > MaxRechargeAmount {
l.Errorw("[Recharge] Recharge amount exceeds maximum limit",
logger.Field("amount", req.Amount),
logger.Field("max", MaxRechargeAmount),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "recharge amount exceeds maximum limit")
}
// find payment method // find payment method
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil { if err != nil {
@ -48,6 +63,17 @@ func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.R
} }
// Calculate the handling fee // Calculate the handling fee
feeAmount := calculateFee(req.Amount, payment) feeAmount := calculateFee(req.Amount, payment)
totalAmount := req.Amount + feeAmount
// Validate total amount after adding fee
if totalAmount > MaxOrderAmount {
l.Errorw("[Recharge] Total amount exceeds maximum limit after fee",
logger.Field("amount", totalAmount),
logger.Field("max", MaxOrderAmount),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "total amount exceeds maximum limit")
}
// query user is new purchase or renewal // query user is new purchase or renewal
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil { if err != nil {
@ -59,7 +85,7 @@ func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.R
OrderNo: tool.GenerateTradeNo(), OrderNo: tool.GenerateTradeNo(),
Type: 4, Type: 4,
Price: req.Amount, Price: req.Amount,
Amount: req.Amount + feeAmount, Amount: totalAmount,
FeeAmount: feeAmount, FeeAmount: feeAmount,
PaymentId: payment.Id, PaymentId: payment.Id,
Method: payment.Platform, Method: payment.Platform,

View File

@ -50,6 +50,12 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
req.Quantity = 1 req.Quantity = 1
} }
// Validate quantity limit
if req.Quantity > MaxQuantity {
l.Errorw("[Renewal] Quantity exceeds maximum limit", logger.Field("quantity", req.Quantity), logger.Field("max", MaxQuantity))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "quantity exceeds maximum limit of %d", MaxQuantity)
}
orderNo := tool.GenerateTradeNo() orderNo := tool.GenerateTradeNo()
// find user subscribe // find user subscribe
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
@ -75,6 +81,17 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
price := sub.UnitPrice * req.Quantity price := sub.UnitPrice * req.Quantity
amount := int64(float64(price) * discount) amount := int64(float64(price) * discount)
discountAmount := price - amount discountAmount := price - amount
// Validate amount to prevent overflow
if amount > MaxOrderAmount {
l.Errorw("[Renewal] Order amount exceeds maximum limit",
logger.Field("amount", amount),
logger.Field("max", MaxOrderAmount),
logger.Field("user_id", u.Id),
logger.Field("subscribe_id", sub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
var coupon int64 = 0 var coupon int64 = 0
if req.Coupon != "" { if req.Coupon != "" {
couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon)
@ -134,6 +151,15 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
amount += feeAmount amount += feeAmount
// Final validation after adding fee
if amount > MaxOrderAmount {
l.Errorw("[Renewal] Final order amount exceeds maximum limit after fee",
logger.Field("amount", amount),
logger.Field("max", MaxOrderAmount),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
// create order // create order
orderInfo := order.Order{ orderInfo := order.Order{
UserId: u.Id, UserId: u.Id,

View File

@ -51,6 +51,7 @@ func NewPurchaseCheckoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *
// PurchaseCheckout processes the checkout for an order using the specified payment method // PurchaseCheckout processes the checkout for an order using the specified payment method
// It validates the order, retrieves payment configuration, and routes to the appropriate payment handler // It validates the order, retrieves payment configuration, and routes to the appropriate payment handler
func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest) (resp *types.CheckoutOrderResponse, err error) { func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest) (resp *types.CheckoutOrderResponse, err error) {
// Validate and retrieve order information // Validate and retrieve order information
orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil { if err != nil {
@ -76,6 +77,7 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
// Process EPay payment - generates payment URL for redirect // Process EPay payment - generates payment URL for redirect
url, err := l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl) url, err := l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl)
if err != nil { if err != nil {
l.Logger.Error("[PurchaseCheckout] epay error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error())
} }
resp = &types.CheckoutOrderResponse{ resp = &types.CheckoutOrderResponse{
@ -274,6 +276,7 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
// Convert order amount to CNY using current exchange rate // Convert order amount to CNY using current exchange rate
amount, err = l.queryExchangeRate("CNY", info.Amount) amount, err = l.queryExchangeRate("CNY", info.Amount)
if err != nil { if err != nil {
l.Logger.Error("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error()))
return "", err return "", err
} }
} else { } else {
@ -382,6 +385,11 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
// Convert cents to decimal amount // Convert cents to decimal amount
amount = float64(src) / float64(100) amount = float64(src) / float64(100)
// No conversion needed if target currency matches system currency
if to == l.svcCtx.Config.Currency.Unit {
return amount, nil
}
if l.svcCtx.ExchangeRate != 0 && to == "CNY" { if l.svcCtx.ExchangeRate != 0 && to == "CNY" {
amount = amount * l.svcCtx.ExchangeRate amount = amount * l.svcCtx.ExchangeRate
return amount, nil return amount, nil
@ -395,6 +403,7 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount
// Convert currency if system currency differs from target currency // Convert currency if system currency differs from target currency
result, err := exchangeRate.GetExchangeRete(l.svcCtx.Config.Currency.Unit, to, l.svcCtx.Config.Currency.AccessKey, 1) result, err := exchangeRate.GetExchangeRete(l.svcCtx.Config.Currency.Unit, to, l.svcCtx.Config.Currency.AccessKey, 1)
if err != nil { if err != nil {
l.Logger.Error("[PurchaseCheckout] QueryExchangeRate error", logger.Field("error", err.Error()))
return 0, err return 0, err
} }
l.svcCtx.ExchangeRate = result l.svcCtx.ExchangeRate = result

View File

@ -24,6 +24,7 @@ type SecurityConfig struct {
RealityPublicKey string `json:"reality_public_key"` RealityPublicKey string `json:"reality_public_key"`
RealityShortId string `json:"reality_short_id"` RealityShortId string `json:"reality_short_id"`
RealityMldsa65seed string `json:"reality_mldsa65seed"` RealityMldsa65seed string `json:"reality_mldsa65seed"`
PaddingScheme string `json:"padding_scheme"`
} }
type TransportConfig struct { type TransportConfig struct {

View File

@ -199,6 +199,7 @@ func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]inter
RealityPrivateKey: config.RealityPrivateKey, RealityPrivateKey: config.RealityPrivateKey,
RealityPublicKey: config.RealityPublicKey, RealityPublicKey: config.RealityPublicKey,
RealityShortId: config.RealityShortId, RealityShortId: config.RealityShortId,
PaddingScheme: config.PaddingScheme,
}, },
} }
case Tuic: case Tuic:

View File

@ -333,14 +333,14 @@ type CreateNodeRequest struct {
type CreateOrderRequest struct { type CreateOrderRequest struct {
UserId int64 `json:"user_id" validate:"required"` UserId int64 `json:"user_id" validate:"required"`
Type uint8 `json:"type" validate:"required"` Type uint8 `json:"type" validate:"required"`
Quantity int64 `json:"quantity,omitempty"` Quantity int64 `json:"quantity,omitempty" validate:"omitempty,lte=1000"`
Price int64 `json:"price" validate:"required"` Price int64 `json:"price" validate:"required,gte=0,lte=2000000000"`
Amount int64 `json:"amount" validate:"required"` Amount int64 `json:"amount" validate:"required,gte=0,lte=2147483647"`
Discount int64 `json:"discount,omitempty"` Discount int64 `json:"discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
CouponDiscount int64 `json:"coupon_discount,omitempty"` CouponDiscount int64 `json:"coupon_discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Commission int64 `json:"commission"` Commission int64 `json:"commission" validate:"gte=0,lte=2000000000"`
FeeAmount int64 `json:"fee_amount" validate:"required"` FeeAmount int64 `json:"fee_amount" validate:"required,gte=0,lte=2000000000"`
PaymentId int64 `json:"payment_id" validate:"required"` PaymentId int64 `json:"payment_id" validate:"required"`
TradeNo string `json:"trade_no,omitempty"` TradeNo string `json:"trade_no,omitempty"`
Status uint8 `json:"status,omitempty"` Status uint8 `json:"status,omitempty"`
@ -1602,7 +1602,7 @@ type PubilcVerifyCodeConfig struct {
type PurchaseOrderRequest struct { type PurchaseOrderRequest struct {
SubscribeId int64 `json:"subscribe_id"` SubscribeId int64 `json:"subscribe_id"`
Quantity int64 `json:"quantity" validate:"required,gt=0"` Quantity int64 `json:"quantity" validate:"required,gt=0,lte=1000"`
Payment int64 `json:"payment,omitempty"` Payment int64 `json:"payment,omitempty"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
} }
@ -1814,7 +1814,7 @@ type QuotaTask struct {
} }
type RechargeOrderRequest struct { type RechargeOrderRequest struct {
Amount int64 `json:"amount"` Amount int64 `json:"amount" validate:"required,gt=0,lte=2000000000"`
Payment int64 `json:"payment"` Payment int64 `json:"payment"`
} }
@ -1877,7 +1877,7 @@ type RegisterLog struct {
type RenewalOrderRequest struct { type RenewalOrderRequest struct {
UserSubscribeID int64 `json:"user_subscribe_id"` UserSubscribeID int64 `json:"user_subscribe_id"`
Quantity int64 `json:"quantity"` Quantity int64 `json:"quantity" validate:"lte=1000"`
Payment int64 `json:"payment"` Payment int64 `json:"payment"`
Coupon string `json:"coupon,omitempty"` Coupon string `json:"coupon,omitempty"`
} }

View File

@ -6,10 +6,11 @@ import (
"time" "time"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/perfect-panel/server/pkg/logger"
) )
const ( const (
Url = "https://api.exchangerate.host" Url = "https://api.apilayer.com"
) )
type Response struct { type Response struct {
@ -40,15 +41,17 @@ func GetExchangeRete(form, to, access string, amount float64) (float64, error) {
"from": form, "from": form,
"to": to, "to": to,
"amount": amountStr, "amount": amountStr,
"access_key": access,
}) })
resp := new(Response) result := new(Response)
_, err := client.R().SetResult(resp).Get("/convert") resp, err := client.R().SetHeader("apikey", access).SetResult(result).Get("/currency_data/convert")
if err != nil { if err != nil {
return 0, err return 0, err
} }
if !resp.Success { if !result.Success {
logger.Info("Exchange Rate Response: ", resp.String())
return 0, errors.New("exchange rate failed") return 0, errors.New("exchange rate failed")
} }
return resp.Result, nil return result.Result, nil
} }

View File

@ -4,7 +4,7 @@ import "testing"
func TestGetExchangeRete(t *testing.T) { func TestGetExchangeRete(t *testing.T) {
t.Skip("skip TestGetExchangeRete") t.Skip("skip TestGetExchangeRete")
result, err := GetExchangeRete("USD", "CNY", "90734e5af4f5353114cdaf3bb9c3f2e3", 1) result, err := GetExchangeRete("USD", "CNY", "", 1)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }