mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
feat: get support chain api
This commit is contained in:
@@ -108,6 +108,97 @@ func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, e
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// CountActiveSubOrders counts sub-orders with status=WaitPay under a parent.
|
||||
func CountActiveSubOrders(parentTradeId string) (int64, error) {
|
||||
var count int64
|
||||
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("parent_trade_id = ?", parentTradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// GetSubOrderByTokenNetwork finds an existing active sub-order matching token+network under a parent.
|
||||
func GetSubOrderByTokenNetwork(parentTradeId string, token string, network string) (*mdb.Orders, error) {
|
||||
order := new(mdb.Orders)
|
||||
err := dao.Mdb.Model(order).
|
||||
Where("parent_trade_id = ?", parentTradeId).
|
||||
Where("token = ?", token).
|
||||
Where("network = ?", network).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Limit(1).
|
||||
Find(order).Error
|
||||
return order, err
|
||||
}
|
||||
|
||||
// GetSiblingSubOrders returns active sub-orders under the same parent, excluding the given trade_id.
|
||||
func GetSiblingSubOrders(parentTradeId string, excludeTradeId string) ([]mdb.Orders, error) {
|
||||
var orders []mdb.Orders
|
||||
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("parent_trade_id = ?", parentTradeId).
|
||||
Where("trade_id != ?", excludeTradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// MarkParentOrderSuccess updates the parent order with the sub-order's payment details.
|
||||
// Token and network are NOT overwritten — the parent keeps its original values.
|
||||
func MarkParentOrderSuccess(parentTradeId string, sub *mdb.Orders) (bool, error) {
|
||||
result := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", parentTradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Updates(map[string]interface{}{
|
||||
"status": mdb.StatusPaySuccess,
|
||||
"block_transaction_id": sub.BlockTransactionId,
|
||||
"callback_confirm": mdb.CallBackConfirmNo,
|
||||
"actual_amount": sub.ActualAmount,
|
||||
"receive_address": sub.ReceiveAddress,
|
||||
})
|
||||
return result.RowsAffected > 0, result.Error
|
||||
}
|
||||
|
||||
// MarkOrderSelected sets is_selected=true for the given trade_id.
|
||||
func MarkOrderSelected(tradeId string) error {
|
||||
return dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeId).
|
||||
Update("is_selected", true).Error
|
||||
}
|
||||
|
||||
// RefreshOrderExpiration resets created_at to now so the expiration timer restarts.
|
||||
// Called on the parent order when a sub-order is created or returned.
|
||||
func RefreshOrderExpiration(tradeId string) error {
|
||||
return dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeId).
|
||||
Update("created_at", time.Now()).Error
|
||||
}
|
||||
|
||||
// ResetCallbackConfirmOk sets callback_confirm back to Ok.
|
||||
// Prevents the callback worker from retrying a sub-order with an empty notify_url.
|
||||
func ResetCallbackConfirmOk(tradeId string) error {
|
||||
return dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeId).
|
||||
Update("callback_confirm", mdb.CallBackConfirmOk).Error
|
||||
}
|
||||
|
||||
// GetActiveSubOrders returns all active sub-orders under a parent.
|
||||
func GetActiveSubOrders(parentTradeId string) ([]mdb.Orders, error) {
|
||||
var orders []mdb.Orders
|
||||
err := dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("parent_trade_id = ?", parentTradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
|
||||
// ExpireOrderByTradeId marks a single order as expired if still waiting.
|
||||
func ExpireOrderByTradeId(tradeId string) error {
|
||||
return dao.Mdb.Model(&mdb.Orders{}).
|
||||
Where("trade_id = ?", tradeId).
|
||||
Where("status = ?", mdb.StatusWaitPay).
|
||||
Update("status", mdb.StatusExpired).Error
|
||||
}
|
||||
|
||||
// GetTradeIdByWalletAddressAndAmountAndToken resolves the reserved trade id by network, address, token and amount.
|
||||
func GetTradeIdByWalletAddressAndAmountAndToken(network string, address string, token string, amount float64) (string, error) {
|
||||
scaledAmount, _ := normalizeLockAmount(amount)
|
||||
|
||||
@@ -8,9 +8,14 @@ const (
|
||||
CallBackConfirmNo = 2
|
||||
)
|
||||
|
||||
const (
|
||||
PaymentTypeEpay = "Epay"
|
||||
)
|
||||
|
||||
type Orders struct {
|
||||
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id"`
|
||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id"`
|
||||
OrderId string `gorm:"column:order_id;uniqueIndex:orders_order_id_uindex" json:"order_id"` // the order id is generated by client, and will notify client when order is paid
|
||||
ParentTradeId string `gorm:"column:parent_trade_id;index:idx_orders_parent_trade_id;default:''" json:"parent_trade_id"`
|
||||
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_id"`
|
||||
Amount float64 `gorm:"column:amount" json:"amount"`
|
||||
Currency string `gorm:"column:currency" json:"currency"`
|
||||
@@ -21,8 +26,11 @@ type Orders struct {
|
||||
Status int `gorm:"column:status;default:1" json:"status"`
|
||||
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
|
||||
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_url"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"`
|
||||
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"`
|
||||
IsSelected bool `gorm:"column:is_selected;default:false" json:"is_selected"`
|
||||
PaymentType string `gorm:"column:payment_type" json:"payment_type"`
|
||||
BaseModel
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const (
|
||||
const (
|
||||
NetworkTron = "tron"
|
||||
NetworkSolana = "solana"
|
||||
NetworkEthereum = "eth"
|
||||
NetworkEthereum = "ethereum"
|
||||
NetworkBsc = "bsc"
|
||||
NetworkPolygon = "polygon"
|
||||
NetworkPlasma = "plasma"
|
||||
|
||||
@@ -12,6 +12,8 @@ type CreateTransactionRequest struct {
|
||||
NotifyUrl string `json:"notify_url" validate:"required"`
|
||||
Signature string `json:"signature" validate:"required"`
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
Name string `json:"name"`
|
||||
PaymentType string `json:"payment_type"`
|
||||
}
|
||||
|
||||
func (r CreateTransactionRequest) Translates() map[string]string {
|
||||
@@ -36,3 +38,18 @@ type OrderProcessingRequest struct {
|
||||
TradeId string
|
||||
BlockTransactionId string
|
||||
}
|
||||
|
||||
// SwitchNetworkRequest 切换支付网络
|
||||
type SwitchNetworkRequest struct {
|
||||
TradeId string `json:"trade_id" validate:"required"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
Network string `json:"network" validate:"required"`
|
||||
}
|
||||
|
||||
func (r SwitchNetworkRequest) Translates() map[string]string {
|
||||
return validate.MS{
|
||||
"TradeId": "订单号",
|
||||
"Token": "币种",
|
||||
"Network": "网络",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,3 +25,16 @@ type OrderNotifyResponse struct {
|
||||
Signature string `json:"signature"` // 签名
|
||||
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期
|
||||
}
|
||||
|
||||
// OrderNotifyResponseEpay epay订单异步回调结构体
|
||||
type OrderNotifyResponseEpay struct {
|
||||
PID int `json:"pid"` // 商户ID
|
||||
TradeNo string `json:"trade_no"` // 平台订单号
|
||||
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
||||
Type string `json:"type"` // 订单类型
|
||||
Name string `json:"name"` // 商品名称
|
||||
Money string `json:"money"` // 订单金额,保留4位小数
|
||||
Sign string `json:"sign"` // 签名
|
||||
SignType string `json:"sign_type"` // 签名类型 // MD5
|
||||
TradeStatus string `json:"trade_status"` // 订单状态 // only has "TRADE_SUCCESS" for now
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type CheckoutCounterResponse struct {
|
||||
ExpirationTime int64 `json:"expiration_time"` // 过期时间 时间戳
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
CreatedAt int64 `json:"created_at"` // 订单创建时间 时间戳
|
||||
IsSelected bool `json:"is_selected"`
|
||||
}
|
||||
|
||||
type CheckStatusResponse struct {
|
||||
|
||||
@@ -95,6 +95,8 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
|
||||
Status: mdb.StatusWaitPay,
|
||||
NotifyUrl: req.NotifyUrl,
|
||||
RedirectUrl: req.RedirectUrl,
|
||||
Name: req.Name,
|
||||
PaymentType: req.PaymentType,
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, order); err != nil {
|
||||
tx.Rollback()
|
||||
@@ -155,6 +157,69 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
|
||||
if err = data.UnLockTransaction(req.Network, req.ReceiveAddress, req.Token, req.Amount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock transaction after pay success failed, trade_id=%s, err=%v", req.TradeId, err)
|
||||
}
|
||||
|
||||
// Load order to check parent-child relationship
|
||||
order, err := data.GetOrderInfoByTradeId(req.TradeId)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parent order paid directly: expire all sub-orders and release their locks
|
||||
if order.ParentTradeId == "" {
|
||||
subs, subErr := data.GetActiveSubOrders(order.TradeId)
|
||||
if subErr != nil {
|
||||
log.Sugar.Errorf("[order] get sub-orders for parent failed, trade_id=%s, err=%v", order.TradeId, subErr)
|
||||
return nil
|
||||
}
|
||||
for _, sub := range subs {
|
||||
if err = data.ExpireOrderByTradeId(sub.TradeId); err != nil {
|
||||
log.Sugar.Warnf("[order] expire sub-order failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||
}
|
||||
if err = data.UnLockTransaction(sub.Network, sub.ReceiveAddress, sub.Token, sub.ActualAmount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock sub-order transaction failed, trade_id=%s, err=%v", sub.TradeId, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sub-order should not trigger its own callback (notify_url is empty).
|
||||
// OrderSuccessWithTransaction unconditionally sets callback_confirm=No,
|
||||
// reset it here to prevent the callback worker from retrying an empty URL.
|
||||
if err = data.ResetCallbackConfirmOk(order.TradeId); err != nil {
|
||||
log.Sugar.Warnf("[order] reset sub-order callback_confirm failed, trade_id=%s, err=%v", order.TradeId, err)
|
||||
}
|
||||
|
||||
parent, err := data.GetOrderInfoByTradeId(order.ParentTradeId)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("[order] load parent order failed, parent_trade_id=%s, err=%v", order.ParentTradeId, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mark parent as paid with sub-order's payment details
|
||||
if _, err = data.MarkParentOrderSuccess(parent.TradeId, order); err != nil {
|
||||
log.Sugar.Errorf("[order] mark parent success failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||
}
|
||||
|
||||
// Release parent's own wallet lock
|
||||
if err = data.UnLockTransaction(parent.Network, parent.ReceiveAddress, parent.Token, parent.ActualAmount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock parent transaction failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||
}
|
||||
|
||||
// Expire sibling sub-orders and release their locks
|
||||
siblings, err := data.GetSiblingSubOrders(parent.TradeId, order.TradeId)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("[order] get sibling sub-orders failed, parent_trade_id=%s, err=%v", parent.TradeId, err)
|
||||
return nil
|
||||
}
|
||||
for _, sib := range siblings {
|
||||
if err = data.ExpireOrderByTradeId(sib.TradeId); err != nil {
|
||||
log.Sugar.Warnf("[order] expire sibling failed, trade_id=%s, err=%v", sib.TradeId, err)
|
||||
}
|
||||
if err = data.UnLockTransaction(sib.Network, sib.ReceiveAddress, sib.Token, sib.ActualAmount); err != nil {
|
||||
log.Sugar.Warnf("[order] unlock sibling transaction failed, trade_id=%s, err=%v", sib.TradeId, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -212,3 +277,141 @@ func GetOrderInfoByTradeId(tradeId string) (*mdb.Orders, error) {
|
||||
}
|
||||
return order, nil
|
||||
}
|
||||
|
||||
const MaxSubOrders = 2
|
||||
|
||||
// SwitchNetwork creates or returns an existing sub-order for a different token+network.
|
||||
func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounterResponse, error) {
|
||||
gCreateTransactionLock.Lock()
|
||||
defer gCreateTransactionLock.Unlock()
|
||||
|
||||
token := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||
network := strings.ToLower(strings.TrimSpace(req.Network))
|
||||
|
||||
// 1. Load parent order
|
||||
parent, err := data.GetOrderInfoByTradeId(req.TradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parent.ID <= 0 {
|
||||
return nil, constant.OrderNotExists
|
||||
}
|
||||
if parent.ParentTradeId != "" {
|
||||
return nil, constant.CannotSwitchSubOrder
|
||||
}
|
||||
if parent.Status != mdb.StatusWaitPay {
|
||||
return nil, constant.OrderNotWaitPay
|
||||
}
|
||||
|
||||
// 2. Same token+network as parent → mark selected and return
|
||||
if strings.EqualFold(parent.Token, token) && strings.EqualFold(parent.Network, network) {
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
parent.IsSelected = true
|
||||
return buildCheckoutResponse(parent), nil
|
||||
}
|
||||
|
||||
// 3. Existing active sub-order for this token+network → return it
|
||||
existing, err := data.GetSubOrderByTokenNetwork(parent.TradeId, token, network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existing.ID > 0 {
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
_ = data.MarkOrderSelected(existing.TradeId)
|
||||
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||
existing.IsSelected = true
|
||||
return buildCheckoutResponse(existing), nil
|
||||
}
|
||||
|
||||
// 4. Check sub-order limit
|
||||
count, err := data.CountActiveSubOrders(parent.TradeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count >= MaxSubOrders {
|
||||
return nil, constant.SubOrderLimitExceeded
|
||||
}
|
||||
|
||||
// 5. Calculate amount for the new network
|
||||
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(parent.Currency))
|
||||
if rate <= 0 {
|
||||
return nil, constant.RateAmountErr
|
||||
}
|
||||
decimalPayAmount := decimal.NewFromFloat(parent.Amount)
|
||||
decimalTokenAmount := decimalPayAmount.Mul(decimal.NewFromFloat(rate))
|
||||
if decimalTokenAmount.Cmp(decimal.NewFromFloat(UsdtMinimumPaymentAmount)) == -1 {
|
||||
return nil, constant.PayAmountErr
|
||||
}
|
||||
|
||||
// 6. Find and lock wallet
|
||||
walletAddress, err := data.GetAvailableWalletAddressByNetwork(network)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(walletAddress) <= 0 {
|
||||
return nil, constant.NotAvailableWalletAddress
|
||||
}
|
||||
|
||||
subTradeID := GenerateCode()
|
||||
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
|
||||
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(subTradeID, network, token, amount, walletAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if availableAddress == "" {
|
||||
return nil, constant.NotAvailableAmountErr
|
||||
}
|
||||
|
||||
// 7. Create sub-order
|
||||
tx := dao.Mdb.Begin()
|
||||
subOrder := &mdb.Orders{
|
||||
TradeId: subTradeID,
|
||||
OrderId: subTradeID, // sub-order uses its own trade_id as order_id (unique constraint)
|
||||
ParentTradeId: parent.TradeId,
|
||||
Amount: parent.Amount,
|
||||
Currency: parent.Currency,
|
||||
ActualAmount: availableAmount,
|
||||
ReceiveAddress: availableAddress,
|
||||
Token: token,
|
||||
Network: network,
|
||||
Status: mdb.StatusWaitPay,
|
||||
IsSelected: true,
|
||||
NotifyUrl: "",
|
||||
RedirectUrl: parent.RedirectUrl,
|
||||
Name: parent.Name,
|
||||
CallBackConfirm: mdb.CallBackConfirmOk, // don't trigger callback on sub-order
|
||||
PaymentType: parent.PaymentType,
|
||||
}
|
||||
if err = data.CreateOrderWithTransaction(tx, subOrder); err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransactionByTradeId(subTradeID)
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit().Error; err != nil {
|
||||
tx.Rollback()
|
||||
_ = data.UnLockTransactionByTradeId(subTradeID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark parent as selected and refresh its expiration to match the sub-order
|
||||
_ = data.MarkOrderSelected(parent.TradeId)
|
||||
_ = data.RefreshOrderExpiration(parent.TradeId)
|
||||
|
||||
return buildCheckoutResponse(subOrder), nil
|
||||
}
|
||||
|
||||
func buildCheckoutResponse(order *mdb.Orders) *response.CheckoutCounterResponse {
|
||||
return &response.CheckoutCounterResponse{
|
||||
TradeId: order.TradeId,
|
||||
Amount: order.Amount,
|
||||
ActualAmount: order.ActualAmount,
|
||||
Token: order.Token,
|
||||
Currency: order.Currency,
|
||||
ReceiveAddress: order.ReceiveAddress,
|
||||
Network: order.Network,
|
||||
ExpirationTime: order.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||
RedirectUrl: order.RedirectUrl,
|
||||
CreatedAt: order.CreatedAt.TimestampMilli(),
|
||||
IsSelected: order.IsSelected,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func GetCheckoutCounterByTradeId(tradeId string) (*response.CheckoutCounterRespo
|
||||
ExpirationTime: orderInfo.CreatedAt.AddMinutes(config.GetOrderExpirationTime()).TimestampMilli(),
|
||||
RedirectUrl: orderInfo.RedirectUrl,
|
||||
CreatedAt: orderInfo.CreatedAt.TimestampMilli(),
|
||||
IsSelected: orderInfo.IsSelected,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user