feat(payment): support placeholder orders for GMPay and EPay

This commit is contained in:
line-6000
2026-06-16 17:57:20 +08:00
parent 04c2b9b0b6
commit 3ab3659fe4
27 changed files with 1755 additions and 134 deletions
+3 -3
View File
@@ -105,12 +105,12 @@ func CountOrdersByStatus() (map[int]int64, error) {
return out, nil
}
// CloseOrderManually transitions a pending order to expired. Only
// touches rows currently in StatusWaitPay so idempotent / safe.
// CloseOrderManually transitions a pending order to expired. It covers both
// payable orders and placeholder orders waiting for token/network selection.
func CloseOrderManually(tradeID string) (bool, error) {
result := dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeID).
Where("status = ?", mdb.StatusWaitPay).
Where("status IN ?", []int{mdb.StatusWaitPay, mdb.StatusWaitSelect}).
Update("status", mdb.StatusExpired)
return result.RowsAffected > 0, result.Error
}
+65 -1
View File
@@ -209,7 +209,7 @@ func SaveCallBackOrdersResp(order *mdb.Orders) error {
func UpdateOrderIsExpirationById(id uint64, expirationCutoff time.Time) (bool, error) {
result := dao.Mdb.Model(mdb.Orders{}).
Where("id = ?", id).
Where("status = ?", mdb.StatusWaitPay).
Where("status IN ?", []int{mdb.StatusWaitPay, mdb.StatusWaitSelect}).
Where("created_at <= ?", expirationCutoff).
Update("status", mdb.StatusExpired)
return result.RowsAffected > 0, result.Error
@@ -225,6 +225,15 @@ func CountActiveSubOrders(parentTradeId string) (int64, error) {
return count, err
}
// CountSubOrders counts all sub-orders ever created under a parent.
func CountSubOrders(parentTradeId string) (int64, error) {
var count int64
err := dao.Mdb.Model(&mdb.Orders{}).
Where("parent_trade_id = ?", parentTradeId).
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)
@@ -288,6 +297,61 @@ func MarkOrderSelected(tradeId string) error {
Update("is_selected", true).Error
}
// CompleteWaitSelectOrder fills a placeholder order with concrete chain fields
// and makes it payable. It only updates status=WaitSelect rows so concurrent
// switch-network attempts cannot overwrite a payable order.
func CompleteWaitSelectOrder(tradeID string, network string, token string, receiveAddress string, actualAmount float64) (bool, error) {
result := dao.Mdb.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeID).
Where("status = ?", mdb.StatusWaitSelect).
Updates(map[string]interface{}{
"status": mdb.StatusWaitPay,
"network": strings.ToLower(strings.TrimSpace(network)),
"token": strings.ToUpper(strings.TrimSpace(token)),
"receive_address": receiveAddress,
"actual_amount": actualAmount,
"is_selected": false,
"created_at": time.Now(),
})
return result.RowsAffected > 0, result.Error
}
// CompleteWaitSelectOkPayOrderWithTransaction converts a placeholder parent
// directly into an OkPay order. It intentionally does not create a local chain
// lock because OkPay owns the hosted payment target.
func CompleteWaitSelectOkPayOrderWithTransaction(tx *gorm.DB, tradeID string, token string, actualAmount float64) (bool, error) {
result := tx.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeID).
Where("status = ?", mdb.StatusWaitSelect).
Updates(map[string]interface{}{
"status": mdb.StatusWaitPay,
"network": mdb.PaymentProviderOkPay,
"token": strings.ToUpper(strings.TrimSpace(token)),
"receive_address": "OKPAY",
"actual_amount": actualAmount,
"is_selected": false,
"pay_provider": mdb.PaymentProviderOkPay,
"created_at": time.Now(),
})
return result.RowsAffected > 0, result.Error
}
// MarkProviderSwitchParentSelectedWithTransaction marks a switch-network parent
// as selected for a hosted payment provider. It intentionally does not fill
// chain fields or create a transaction lock; the provider child order carries
// the concrete payment target.
func MarkProviderSwitchParentSelectedWithTransaction(tx *gorm.DB, tradeID string) (bool, error) {
result := tx.Model(&mdb.Orders{}).
Where("trade_id = ?", tradeID).
Where("status IN ?", []int{mdb.StatusWaitPay, mdb.StatusWaitSelect}).
Updates(map[string]interface{}{
"status": mdb.StatusWaitPay,
"is_selected": true,
"created_at": time.Now(),
})
return result.RowsAffected > 0, result.Error
}
// ExpireOrderByTradeID marks a waiting order as expired. Used to retire failed
// child-order attempts that should not remain selectable/reusable.
func ExpireOrderByTradeID(tradeId string) error {
+21 -1
View File
@@ -4,6 +4,7 @@ import (
"github.com/GMWalletApp/epusdt/model/dao"
"github.com/GMWalletApp/epusdt/model/mdb"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetProviderOrderByTradeIDAndProvider(tradeID string, provider string) (*mdb.ProviderOrder, error) {
@@ -20,8 +21,27 @@ func CreateProviderOrderWithTransaction(tx *gorm.DB, row *mdb.ProviderOrder) err
return tx.Model(row).Create(row).Error
}
func UpsertProviderOrderCreatingWithTransaction(tx *gorm.DB, row *mdb.ProviderOrder) error {
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "trade_id"}, {Name: "provider"}},
DoUpdates: clause.AssignmentColumns([]string{
"provider_order_id",
"pay_url",
"amount",
"coin",
"status",
"notify_raw",
"updated_at",
}),
}).Create(row).Error
}
func UpdateProviderOrderCreated(tradeID string, provider string, providerOrderID string, payURL string) error {
return dao.Mdb.Model(&mdb.ProviderOrder{}).
return UpdateProviderOrderCreatedWithTransaction(dao.Mdb, tradeID, provider, providerOrderID, payURL)
}
func UpdateProviderOrderCreatedWithTransaction(tx *gorm.DB, tradeID string, provider string, providerOrderID string, payURL string) error {
return tx.Model(&mdb.ProviderOrder{}).
Where("trade_id = ?", tradeID).
Where("provider = ?", provider).
Updates(map[string]interface{}{