add epay route support

This commit is contained in:
line-6000
2026-04-12 18:14:40 +08:00
parent 730ff983f8
commit 32ca778735
9 changed files with 216 additions and 29 deletions
+5
View File
@@ -64,3 +64,8 @@ tron_grid_api_key=
solana_rpc_url= solana_rpc_url=
ethereum_ws_url=wss://ethereum.publicnode.com ethereum_ws_url=wss://ethereum.publicnode.com
# epay
epay_pid=
epay_key=
+8
View File
@@ -340,3 +340,11 @@ func GetSolanaRpcUrl() string {
func GetEthereumWsUrl() string { func GetEthereumWsUrl() string {
return strings.TrimSpace(viper.GetString("ethereum_ws_url")) return strings.TrimSpace(viper.GetString("ethereum_ws_url"))
} }
func GetEpayPid() int {
return viper.GetInt("epay_pid")
}
func GetEpayKey() string {
return viper.GetString("epay_key")
}
+29
View File
@@ -1,6 +1,9 @@
package comm package comm
import ( import (
"fmt"
"log"
"github.com/assimon/luuu/model/request" "github.com/assimon/luuu/model/request"
"github.com/assimon/luuu/model/service" "github.com/assimon/luuu/model/service"
"github.com/assimon/luuu/util/constant" "github.com/assimon/luuu/util/constant"
@@ -22,3 +25,29 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
} }
return c.SucJson(ctx, resp) return c.SucJson(ctx, resp)
} }
func (c *BaseCommController) CreateTransactionAndRedirect(ctx echo.Context) (err error) {
req := new(request.CreateTransactionRequest)
if err = ctx.Bind(req); err != nil {
log.Println("bind request error:", err)
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err = c.ValidateStruct(ctx, req); err != nil {
log.Println("validate request error:", err)
return c.FailJson(ctx, err)
}
resp, err := service.CreateTransaction(req)
if err != nil {
log.Println("create transaction error:", err)
return c.FailJson(ctx, err)
}
fmt.Printf("create transaction response: %+v\n", resp)
tradeID := resp.TradeId
ctx.Redirect(302, "/pay/checkout-counter/"+tradeID)
return nil
}
+7 -1
View File
@@ -8,9 +8,13 @@ const (
CallBackConfirmNo = 2 CallBackConfirmNo = 2
) )
const (
PaymentTypeEpay = "Epay"
)
type Orders struct { type Orders struct {
TradeId string `gorm:"column:trade_id;uniqueIndex:orders_trade_id_uindex" json:"trade_id"` 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
BlockTransactionId string `gorm:"index:orders_block_transaction_id_index;column:block_transaction_id" json:"block_transaction_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"` Amount float64 `gorm:"column:amount" json:"amount"`
Currency string `gorm:"column:currency" json:"currency"` Currency string `gorm:"column:currency" json:"currency"`
@@ -21,8 +25,10 @@ type Orders struct {
Status int `gorm:"column:status;default:1" json:"status"` Status int `gorm:"column:status;default:1" json:"status"`
NotifyUrl string `gorm:"column:notify_url" json:"notify_url"` NotifyUrl string `gorm:"column:notify_url" json:"notify_url"`
RedirectUrl string `gorm:"column:redirect_url" json:"redirect_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"` CallbackNum int `gorm:"column:callback_num;default:0" json:"callback_num"`
CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"` CallBackConfirm int `gorm:"column:callback_confirm;default:2" json:"callback_confirm"`
PaymentType string `gorm:"column:payment_type" json:"payment_type"`
BaseModel BaseModel
} }
+2
View File
@@ -12,6 +12,8 @@ type CreateTransactionRequest struct {
NotifyUrl string `json:"notify_url" validate:"required"` NotifyUrl string `json:"notify_url" validate:"required"`
Signature string `json:"signature" validate:"required"` Signature string `json:"signature" validate:"required"`
RedirectUrl string `json:"redirect_url"` RedirectUrl string `json:"redirect_url"`
Name string `json:"name"`
PaymentType string `json:"payment_type"`
} }
func (r CreateTransactionRequest) Translates() map[string]string { func (r CreateTransactionRequest) Translates() map[string]string {
+13
View File
@@ -25,3 +25,16 @@ type OrderNotifyResponse struct {
Signature string `json:"signature"` // 签名 Signature string `json:"signature"` // 签名
Status int `json:"status"` // 1:等待支付,2:支付成功,3:已过期 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
}
+2
View File
@@ -95,6 +95,8 @@ func CreateTransaction(req *request.CreateTransactionRequest) (*response.CreateT
Status: mdb.StatusWaitPay, Status: mdb.StatusWaitPay,
NotifyUrl: req.NotifyUrl, NotifyUrl: req.NotifyUrl,
RedirectUrl: req.RedirectUrl, RedirectUrl: req.RedirectUrl,
Name: req.Name,
PaymentType: req.PaymentType,
} }
if err = data.CreateOrderWithTransaction(tx, order); err != nil { if err = data.CreateOrderWithTransaction(tx, order); err != nil {
tx.Rollback() tx.Rollback()
+53
View File
@@ -2,7 +2,10 @@ package mq
import ( import (
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@@ -162,6 +165,54 @@ func processCallback(tradeID string) {
} }
func sendOrderCallback(order *mdb.Orders) error { func sendOrderCallback(order *mdb.Orders) error {
switch order.PaymentType {
case mdb.PaymentTypeEpay:
// 构造 EPay 标准回调参数
notifyData := response.OrderNotifyResponseEpay{
PID: config.GetEpayPid(),
TradeNo: order.TradeId, // epusdt 订单号作为 EPay 平台订单号
OutTradeNo: order.OrderId, // 注意:EPay 回调要求商户订单号使用 out_trade_no 参数
Type: "alipay",
Name: order.Name,
Money: fmt.Sprintf("%.4f", order.Amount),
TradeStatus: "TRADE_SUCCESS",
}
signstr2, err := sign.Get(notifyData, config.GetEpayKey())
if err != nil {
return err
}
// 使用 form-encoded POSTEPay 标准协议格式)
formData := url.Values{
"pid": {fmt.Sprintf("%d", notifyData.PID)},
"trade_no": {notifyData.TradeNo},
"out_trade_no": {notifyData.OutTradeNo},
"type": {notifyData.Type},
"name": {notifyData.Name},
"money": {notifyData.Money},
"trade_status": {notifyData.TradeStatus},
"sign": {signstr2},
"sign_type": {"MD5"},
}
resp, err := http.PostForm(order.NotifyUrl, formData)
if err != nil {
return err
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("notify_url response status: %d, body: %s\n", resp.StatusCode, string(responseBody))
default:
client := http_client.GetHttpClient() client := http_client.GetHttpClient()
orderResp := response.OrderNotifyResponse{ orderResp := response.OrderNotifyResponse{
TradeId: order.TradeId, TradeId: order.TradeId,
@@ -192,6 +243,8 @@ func sendOrderCallback(order *mdb.Orders) error {
if string(resp.Body()) != "ok" { if string(resp.Body()) != "ok" {
return errors.New("not ok") return errors.New("not ok")
} }
}
return nil return nil
} }
+69
View File
@@ -3,11 +3,17 @@ package route
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"github.com/assimon/luuu/config"
"github.com/assimon/luuu/controller/comm" "github.com/assimon/luuu/controller/comm"
"github.com/assimon/luuu/middleware" "github.com/assimon/luuu/middleware"
"github.com/assimon/luuu/model/mdb"
"github.com/assimon/luuu/util/constant"
"github.com/assimon/luuu/util/sign"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -49,6 +55,7 @@ func RegisterRoute(e *echo.Echo) {
return comm.Ctrl.FailJson(ctx, err) return comm.Ctrl.FailJson(ctx, err)
} }
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes)) ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
ctx.Request().ContentLength = int64(len(jsonBytes))
return comm.Ctrl.CreateTransaction(ctx) return comm.Ctrl.CreateTransaction(ctx)
}, middleware.CheckApiSign()) }, middleware.CheckApiSign())
@@ -64,4 +71,66 @@ func RegisterRoute(e *echo.Echo) {
walletV1.GET("/:id", comm.Ctrl.GetWallet) walletV1.GET("/:id", comm.Ctrl.GetWallet)
walletV1.POST("/:id/status", comm.Ctrl.ChangeWalletStatus) walletV1.POST("/:id/status", comm.Ctrl.ChangeWalletStatus)
walletV1.POST("/:id/delete", comm.Ctrl.DeleteWallet) walletV1.POST("/:id/delete", comm.Ctrl.DeleteWallet)
// epay v1 routes
epayV1 := paymentRoute.Group("/epay/v1")
epayV1.GET("/order/create-transaction/submit.php", func(ctx echo.Context) error {
money := ctx.QueryParam("money")
name := ctx.QueryParam("name")
notifyURL := ctx.QueryParam("notify_url")
outTradeNo := ctx.QueryParam("out_trade_no")
returnURL := ctx.QueryParam("return_url")
signstr := ctx.QueryParam("sign")
// signType := ctx.QueryParam("sign_type")
m := map[string]interface{}{
"money": money,
"name": name,
"notify_url": notifyURL,
"out_trade_no": outTradeNo,
"pid": config.GetEpayPid(), // 注意:验签时需要包含 pid 参数
"return_url": returnURL,
}
checkSignature, err := sign.Get(m, config.GetApiAuthToken())
if err != nil {
return constant.SignatureErr
}
if checkSignature != signstr {
return constant.SignatureErr
}
amountFloat, err := strconv.ParseFloat(money, 64)
if err != nil {
return comm.Ctrl.FailJson(ctx, fmt.Errorf("invalid money value: %s", money))
}
body := map[string]interface{}{
"token": "usdt",
"currency": "cny",
"network": "tron",
"amount": amountFloat,
"notify_url": notifyURL,
"order_id": outTradeNo,
"redirect_url": returnURL,
"signature": signstr,
"name": name,
"payment_type": mdb.PaymentTypeEpay,
}
ctx.Set("request_body", body)
jsonBytes, err := json.Marshal(body)
if err != nil {
return comm.Ctrl.FailJson(ctx, err)
}
ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes))
ctx.Request().ContentLength = int64(len(jsonBytes))
ctx.Request().Method = http.MethodPost
ctx.Request().Header.Set("Content-Type", "application/json")
return comm.Ctrl.CreateTransactionAndRedirect(ctx)
})
} }