From 32ca778735ab18396e68cfaf994917c8db670a74 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:14:40 +0800 Subject: [PATCH] add epay route support --- src/.env.example | 5 ++ src/config/config.go | 8 ++ src/controller/comm/order_controller.go | 29 +++++++ src/model/mdb/orders_mdb.go | 8 +- src/model/request/order_request.go | 2 + src/model/response/order_response.go | 13 +++ src/model/service/order_service.go | 2 + src/mq/worker.go | 109 ++++++++++++++++++------ src/route/router.go | 69 +++++++++++++++ 9 files changed, 216 insertions(+), 29 deletions(-) diff --git a/src/.env.example b/src/.env.example index 18c0d62..b595454 100644 --- a/src/.env.example +++ b/src/.env.example @@ -64,3 +64,8 @@ tron_grid_api_key= solana_rpc_url= ethereum_ws_url=wss://ethereum.publicnode.com + + +# epay +epay_pid= +epay_key= diff --git a/src/config/config.go b/src/config/config.go index a4bc937..fa96b18 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -340,3 +340,11 @@ func GetSolanaRpcUrl() string { func GetEthereumWsUrl() string { return strings.TrimSpace(viper.GetString("ethereum_ws_url")) } + +func GetEpayPid() int { + return viper.GetInt("epay_pid") +} + +func GetEpayKey() string { + return viper.GetString("epay_key") +} diff --git a/src/controller/comm/order_controller.go b/src/controller/comm/order_controller.go index f745711..039a6a9 100644 --- a/src/controller/comm/order_controller.go +++ b/src/controller/comm/order_controller.go @@ -1,6 +1,9 @@ package comm import ( + "fmt" + "log" + "github.com/assimon/luuu/model/request" "github.com/assimon/luuu/model/service" "github.com/assimon/luuu/util/constant" @@ -22,3 +25,29 @@ func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) { } 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 + +} diff --git a/src/model/mdb/orders_mdb.go b/src/model/mdb/orders_mdb.go index 5d262f5..d5793eb 100644 --- a/src/model/mdb/orders_mdb.go +++ b/src/model/mdb/orders_mdb.go @@ -8,9 +8,13 @@ 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 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 +25,10 @@ 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"` + PaymentType string `gorm:"column:payment_type" json:"payment_type"` BaseModel } diff --git a/src/model/request/order_request.go b/src/model/request/order_request.go index f861088..f2ca801 100644 --- a/src/model/request/order_request.go +++ b/src/model/request/order_request.go @@ -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 { diff --git a/src/model/response/order_response.go b/src/model/response/order_response.go index a04902f..729ab7d 100644 --- a/src/model/response/order_response.go +++ b/src/model/response/order_response.go @@ -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 +} diff --git a/src/model/service/order_service.go b/src/model/service/order_service.go index 4a78c6c..4d00d7f 100644 --- a/src/model/service/order_service.go +++ b/src/model/service/order_service.go @@ -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() diff --git a/src/mq/worker.go b/src/mq/worker.go index 9c11dea..3797c92 100644 --- a/src/mq/worker.go +++ b/src/mq/worker.go @@ -2,7 +2,10 @@ package mq import ( "errors" + "fmt" + "io" "net/http" + "net/url" "strings" "time" @@ -162,36 +165,86 @@ func processCallback(tradeID string) { } func sendOrderCallback(order *mdb.Orders) error { - client := http_client.GetHttpClient() - orderResp := response.OrderNotifyResponse{ - TradeId: order.TradeId, - OrderId: order.OrderId, - Amount: order.Amount, - ActualAmount: order.ActualAmount, - ReceiveAddress: order.ReceiveAddress, - Token: order.Token, - BlockTransactionId: order.BlockTransactionId, - Status: mdb.StatusPaySuccess, - } - signature, err := sign.Get(orderResp, config.GetApiAuthToken()) - if err != nil { - return err - } - orderResp.Signature = signature - resp, err := client.R(). - SetHeader("powered-by", "Epusdt(https://github.com/GMwalletApp/epusdt)"). - SetBody(orderResp). - Post(order.NotifyUrl) - if err != nil { - return err - } - if resp.StatusCode() != http.StatusOK { - return errors.New(resp.Status()) - } - if string(resp.Body()) != "ok" { - return errors.New("not ok") + 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 POST(EPay 标准协议格式) + 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() + orderResp := response.OrderNotifyResponse{ + TradeId: order.TradeId, + OrderId: order.OrderId, + Amount: order.Amount, + ActualAmount: order.ActualAmount, + ReceiveAddress: order.ReceiveAddress, + Token: order.Token, + BlockTransactionId: order.BlockTransactionId, + Status: mdb.StatusPaySuccess, + } + signature, err := sign.Get(orderResp, config.GetApiAuthToken()) + if err != nil { + return err + } + orderResp.Signature = signature + + resp, err := client.R(). + SetHeader("powered-by", "Epusdt(https://github.com/GMwalletApp/epusdt)"). + SetBody(orderResp). + Post(order.NotifyUrl) + if err != nil { + return err + } + if resp.StatusCode() != http.StatusOK { + return errors.New(resp.Status()) + } + if string(resp.Body()) != "ok" { + return errors.New("not ok") + } } + return nil } diff --git a/src/route/router.go b/src/route/router.go index 8ee23db..37a6943 100644 --- a/src/route/router.go +++ b/src/route/router.go @@ -3,11 +3,17 @@ package route import ( "bytes" "encoding/json" + "fmt" "io" "net/http" + "strconv" + "github.com/assimon/luuu/config" "github.com/assimon/luuu/controller/comm" "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" ) @@ -49,6 +55,7 @@ func RegisterRoute(e *echo.Echo) { return comm.Ctrl.FailJson(ctx, err) } ctx.Request().Body = io.NopCloser(bytes.NewBuffer(jsonBytes)) + ctx.Request().ContentLength = int64(len(jsonBytes)) return comm.Ctrl.CreateTransaction(ctx) }, middleware.CheckApiSign()) @@ -64,4 +71,66 @@ func RegisterRoute(e *echo.Echo) { walletV1.GET("/:id", comm.Ctrl.GetWallet) walletV1.POST("/:id/status", comm.Ctrl.ChangeWalletStatus) 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) + + }) }